20.2 Evaluation basics

Here we’ll explore the details of eval() which we briefly mentioned in the last chapter. It has two key arguments: expr and envir. The first argument, expr, is the object to evaluate, typically a symbol or expression74. None of the evaluation functions quote their inputs, so you’ll usually use them with expr() or similar:

x <- 10
eval(expr(x))
#> [1] 10

y <- 2
eval(expr(x + y))
#> [1] 12

The second argument, env, gives the environment in which the expression should be evaluated, i.e. where to look for the values of x, y, and +. By default, this is the current environment, i.e. the calling environment of eval(), but you can override it if you want:

eval(expr(x + y), env(x = 1000))
#> [1] 1002

The first argument is evaluated, not quoted, which can lead to confusing results once if you use a custom environment and forget to manually quote:

eval(print(x + 1), env(x = 1000))
#> [1] 11
#> [1] 11

eval(expr(print(x + 1)), env(x = 1000))
#> [1] 1001

Now that you’ve seen the basics, let’s explore some applications. We’ll focus primarily on base R functions that you might have used before, reimplementing the underlying principles using rlang.

20.2.1 Application: local()

Sometimes you want to perform a chunk of calculation that creates some intermediate variables. The intermediate variables have no long-term use and could be quite large, so you’d rather not keep them around. One approach is to clean up after yourself using rm(); another is to wrap the code in a function and just call it once. A more elegant approach is to use local():

# Clean up variables created earlier
rm(x, y)

foo <- local({
  x <- 10
  y <- 200
  x + y
})

foo
#> [1] 210
x
#> Error in eval(expr, envir, enclos): object 'x' not found
y
#> Error in eval(expr, envir, enclos): object 'y' not found

The essence of local() is quite simple and re-implemented below. We capture the input expression, and create a new environment in which to evaluate it. This is a new environment (so assignment doesn’t affect the existing environment) with the caller environment as parent (so that expr can still access variables in that environment). This effectively emulates running expr as if it was inside a function (i.e. it’s lexically scoped, Section 6.4).

local2 <- function(expr) {
  env <- env(caller_env())
  eval(enexpr(expr), env)
}

foo <- local2({
  x <- 10
  y <- 200
  x + y
})

foo
#> [1] 210
x
#> Error in eval(expr, envir, enclos): object 'x' not found
y
#> Error in eval(expr, envir, enclos): object 'y' not found

Understanding how base::local() works is harder, as it uses eval() and substitute() together in rather complicated ways. Figuring out exactly what’s going on is good practice if you really want to understand the subtleties of substitute() and the base eval() functions, so they are included in the exercises below.

20.2.2 Application: source()

We can create a simple version of source() by combining eval() with parse_expr() from Section 18.4.3. We read in the file from disk, use parse_expr() to parse the string into a list of expressions, and then use eval() to evaluate each element in turn. This version evaluates the code in the caller environment, and invisibly returns the result of the last expression in the file just like base::source().

source2 <- function(path, env = caller_env()) {
  file <- paste(readLines(path, warn = FALSE), collapse = "\n")
  exprs <- parse_exprs(file)

  res <- NULL
  for (i in seq_along(exprs)) {
    res <- eval(exprs[[i]], env)
  }

  invisible(res)
}

The real source() is considerably more complicated because it can echo input and output, and has many other settings that control its behaviour.

20.2.3 Gotcha: function()

There’s one small gotcha that you should be aware of if you’re using eval() and expr() to generate functions:

x <- 10
y <- 20
f <- eval(expr(function(x, y) !!x + !!y))
f
#> function(x, y) !!x + !!y

This function doesn’t look like it will work, but it does:

f()
#> [1] 30

This is because, if available, functions print their srcref attribute (Section 6.2.1), and because srcref is a base R feature it’s unaware of quasiquotation.

To work around this problem, either use new_function() (Section 19.7.4) or remove the srcref attribute:

attr(f, "srcref") <- NULL
f
#> function (x, y) 
#> 10 + 20

20.2.4 Exercises

  1. Carefully read the documentation for source(). What environment does it use by default? What if you supply local = TRUE? How do you provide a custom environment?

  2. Predict the results of the following lines of code:

    eval(expr(eval(expr(eval(expr(2 + 2))))))
    eval(eval(expr(eval(expr(eval(expr(2 + 2)))))))
    expr(eval(expr(eval(expr(eval(expr(2 + 2)))))))
  3. Fill in the function bodies below to re-implement get() using sym() and eval(), andassign() using sym(), expr(), and eval(). Don’t worry about the multiple ways of choosing an environment that get() and assign() support; assume that the user supplies it explicitly.

    # name is a string
    get2 <- function(name, env) {}
    assign2 <- function(name, value, env) {}
  4. Modify source2() so it returns the result of every expression, not just the last one. Can you eliminate the for loop?

  5. We can make base::local() slightly easier to understand by spreading out over multiple lines:

    local3 <- function(expr, envir = new.env()) {
      call <- substitute(eval(quote(expr), envir))
      eval(call, envir = parent.frame())
    }

    Explain how local() works in words. (Hint: you might want to print(call) to help understand what substitute() is doing, and read the documentation to remind yourself what environment new.env() will inherit from.)


  1. All other objects yield themselves when evaluated; i.e. eval(x) yields x, except when x is a symbol or expression.↩︎