Lazy evaluation in eval function

2023-05-22


The lazy evaluation arguments in eval() function indeed confused me for a few days. I realize that problem when I read the Learning R programming(the Chinese edition); There is a part of code like this:

qs <- function(x, range) { 
  range <- substitute(range)
  selector <-eval(range, list(. = length(x)))
  x[selector]

Here, we create a function that quotes the range argument; then evaluate the range expression in a list environment that the dot. represents the length of x. A concrete example is as follows:

x <- 1:10
qs(x, 3:(. - 5))

Issues occur when we try to wrap this kind of function with quoted arguments.

trim_margin <- function(x, n) {
  qs(x, (n + 1) : (. - n - 1))
}

The trim_margin function aims to trim n paired-end elements in a vector; However, errors happened: ‘can’t find the object n’;

We know that R finds its variable’s value through the lexical scope; There are three environments related to function; When the function executes, it’s executed in a temporary environment; And its parent environment is its closed environment where the function was defined; That’s lexical scope. However, in the example above, the qs function in trim_margin function was defined in the global environment which doesn’t contain the variable n; The n existed in the execution environment of trim_margin function; That’s the called environment of the qs function(find variable value in a caller environment named dynamic scope); We can get this environment through parent.frame(); However, the eval() function uses the parent.frame() as the default enclose environment of the expression when the argument envir is a list.

r$> eval
function (expr, envir = parent.frame(), enclos = if (is.list(envir) 
    is.pairlist(envir)) parent.frame() else baseenv())
.Internal(eval(expr, envir, enclos))
<bytecode: 0x000001640dd05420>
<environment: namespace:base>

What happens here is lazy evaluation in the function’s argument - the function arguments are only evaluated when needed; A example from Advanced R:

z <- 1
f1 <- function(x = ls()) {
  y <- 2
  x
}

f1() # ls() evaluated in the function execution environment
[1] "x" "y"
f1(ls()) # ls() evaluated in the global environment
[1] "f1" "z"

We can find that the user-supplied ls() executed in the global environment, but the arguments computed as the default value ls() executed in the f1 execution environment; That’s why the eval() in the qs can’t find n although its default enclose environment is the parent.frame() function; The parent frame is computed in the eval execution environment; which returns the qs execution environment as an enclosed environment; However, the n existed in the trim_margin execution environment; To achieve this, just user supplied the eval() enclos argument to parent.frame() which execute in the qs execution environment and returns the caller function of qs- that is the execution environment of trim_margin where variable n existed.

qs <- function(x, range) { 
  range <- substitute(range)
  selector <-eval(range, list(. = length(x)), enclos = parent.frame())
  x[selector]

trim_margin <- function(x, n) {
  qs(x, (n + 1) : (. - n - 1))
}

All perfect!