Chapter 5 Non-Standard Evaluation

The biggest difference between R and Python is not where R starts counting, but its use of lazy evaluation. Nothing truly makes sense in R until we understand how this works.

5.1 Learning Objectives

  • Trace the order of evaluation in function calls.
  • Explain what environments and expressions are and how they relate to one another.
  • Justify the author’s use of ASCII art in the second decade of the 21st Century.

5.2 How does Python evaluate function calls?

Let’s start by looking at a small Python program and its output:

def ones_func(ones_arg):
    return ones_arg + " ones"

def tens_func(tens_arg):
    return ones_func(tens_arg + " tens")

initial = "start"
final = tens_func(initial + " more")
print(final)
start more tens ones

When we call tens_func we pass it initial + " more"; since initial has just been assigned "start", that’s the same as calling tens_func with "start more". tens_func then calls ones_func with "start more tens", and ones_func returns "start more tens ones". But there’s a lot more going on here than first meets the eye. Let’s spell out the steps:

def ones_func(ones_arg):
    ones_temp_1 = ones_arg + " ones"
    return ones_temp_1

def tens_func(tens_arg):
    tens_temp_1 = tens_arg + " tens"
    tens_temp_2 = ones_func(tens_temp_1)
    return tens_temp_2

initial = "start"
global_temp_1 = initial + " more"
final = tens_func(global_temp_1)
print(final)
start more tens ones

Step 1: we assign "start" to initial at the global level:

Python Step 1

Figure 5.1: Python Step 1

Step 2: we ask Python to call tens_func(initial + "more"), so it creates a temporary variable to hold the result of the concatenation before calling tens_func:

Python Step 2

Figure 5.2: Python Step 2

Step 3: Python creates a new stack frame to hold the call to tens_func:

Python Step 3

Figure 5.3: Python Step 3

Note that tens_arg points to the same thing in memory as global_temp_1, since Python passes everything by reference.

Step 4: we ask Python to call ones_func(tens_arg + " tens"), so it creates another temporary:

Python Step 4

Figure 5.4: Python Step 4

Step 5: Python creates a new stack frame to hold the call to ones_func:

Python Step 5

Figure 5.5: Python Step 5

Step 6: Python creates a temporary to hold ones_arg + "ones":

Python Step 6

Figure 5.6: Python Step 6

Step 7: Python returns from ones_func and puts its result in yet another temporary variable in tens_func:

Python Step 7

Figure 5.7: Python Step 7

Step 8: Python returns from tens_func and puts its result in final:

Python Step 8

Figure 5.8: Python Step 8

The most important thing here is that Python evaluates expressions before it calls functions, and passes the results of those evaluations to the functions. This is called eager evaluation, and is what most widely-used programming languages do.

5.3 How does R evaluate the same kind of thing?

In contrast, R uses lazy evaluation. Here’s an R program that’s roughly equivalent to the Python shown above:

ones_func <- function(ones_arg) {
  paste(ones_arg, "ones")
}

tens_func <- function(tens_arg) {
  ones_func(paste(tens_arg, "tens"))
}

initial <- "start"
final <- tens_func(paste(initial, "more"))
print(final)
[1] "start more tens ones"

And here it is with the intermediate steps spelled out in a syntax I just made up:

ones_func <- function(ones_arg) {
  ones_arg.RESOLVE(@tens_func@, paste(tens_arg, "tens"), "start more tens")
  ones_temp_1 <- paste(ones_arg, "ones")
  return(ones_temp_1)
}

tens_func <- function(tens_arg) {
  tens_arg.RESOLVE(@global@, paste(initial, "more"), "start more")
  tens_temp_1 <- PROMISE(@tens_func@, paste(tens_arg, "tens"), ____)
  tens_temp_2 <- ones_func(paste(tens_temp_1))
  return(tens_temp_2)
}

initial <- "start"
global_temp_1 <- PROMISE(@global@, paste(initial, "more"), ____)
final <- tens_func(global_temp_1)
print(final)

While the original code looked much like our Python, the evaluation trace is very different, and hinges on the fact that an expression in a programming language can be represented as a data structure.

What’s an Expression?

An expression is anything that has a value. The simplest expressions are literal values like the number 1, the string "stuff", and the Boolean TRUE. A variable like least is also an expression: its value is whatever the variable currently refers to.

Complex expressions are built out of simpler expressions: 1 + 2 is an expression that uses + to combine 1 and 2, while the expression c(10, 20, 30) uses the function c to create a vector out of the values 10, 20, 30. Expressions are often drawn as trees like this:

    +
   / \
  1   2

When Python (or R, or any other language) reads a program, it parses the text and builds trees like the one shown above to represent what the program is supposed to do. Processing that data structure to find its value is called evaluating the expression.

Most modern languages allow us to build trees ourselves, either by concatenating strings to create program text and then asking the language to parse the result:

left <- '1'
right <- '2'
op <- '+'
combined <- paste(left, op, right)
tree <- parse(text = combined)

or by calling functions. The function-based approach is safer and more flexible; see Wickham (2019) for details.

Step 1: we assign “start” to initial in the global environment:

R Step 1

Figure 5.9: R Step 1

Step 2: we ask R to call tens_func(initial + "more"), so it creates a promise to hold:

  • the environment we’re in (which I’m surrounding with @),
  • the expression we’re passing to the function, and
  • the value of that expression (which I’m showing as ____, since it’s initially empty).
R Step 2

Figure 5.10: R Step 2

and in Step 3, passes that into tens_func:

R Step 3

Figure 5.11: R Step 3

Crucially, the promise in tens_func remembers that it was created in the global environment: it’s eventually going to need a value for initial, so it needs to know where to look to find the right one.

Step 4: since the very next thing we ask for is paste(tens_arg, "tens"), R needs a value for tens_arg. To get it, R evaluates the promise that tens_arg refers to:

R Step 4

(#fig:r-step-4 )R Step 4

This evaluation happens after tens_func has been called, not before as in Python, which is why this scheme is called “lazy” evaluation. Once a promise has been resolved, R uses its value, and that value never changes.

Steps 5: tens_func wants to call ones_func, so R creates another promise to record what’s being passed into ones_func:

R Step 5

Figure 5.12: R Step 5

Step 6: R calls ones_func, binding the newly-created promise to ones_arg as it does so:

R Step 6

Figure 5.13: R Step 6

Step 7: R needs a value for ones_arg to pass to paste, so it resolves the promise:

R Step 7

Figure 5.14: R Step 7

Step 8: ones_func uses paste to concatenate strings:

R Step 8

Figure 5.15: R Step 8

Step 9: ones_func returns:

R Step 9

Figure 5.16: R Step 9

Step 10: tens_func returns:

R Step 10

Figure 5.17: R Step 10

We got the same answer, but in a significantly different way. Each time we passed something into a function, R created a promise to record what it was and where it came from, and then resolved the promise when the value was needed. R always does this: if we call:

sign(2)

then behind the scenes, R is creating a promise and passing it to sign, where it is automatically resolved to get the number 2 when its value is needed. (If I wanted to be thorough, I would have shown the promises passed into paste at each stage of execution above, but that’s a lot of typing.)

5.4 Why is lazy evaluation useful?

R’s lazy evaluation seems pointless if it always produces the same answer as Python’s eager evaluation, but as you may already have guessed, it doesn’t have to. To see how powerful lazy evaluation can be, let’s create an expression of our own:

my_expr <- expr(red)

Displaying the value of my_expr isn’t very exciting:

my_expr
red

but what kind of thing is it?

typeof(my_expr)
[1] "symbol"

A symbol is a kind of expression. It is not a string (though strings can be converted to symbols and symbols to strings) nor is it a value—not yet. If we try to get the value it refers to, R displays an error message:

eval(my_expr)
Error in eval(my_expr): object 'red' not found

We haven’t created a variable called red, so R cannot evaluate an expression that asks for it.

But what if we create such a variable now and then re-evaluate the expression?

red <- "this is red"
eval(my_expr)
[1] "this is red"

More usefully, what if we create something that has a value for red:

color_data <- tribble(
  ~red, ~green,
     1,     10,
    20,      2
)
color_data
# A tibble: 2 x 2
    red green
  <dbl> <dbl>
1     1    10
2    20     2

and then ask R to evaluate our expression in the context of that tibble:

eval(my_expr, color_data)
[1]  1 20

When we do this, eval looks for definitions of variables in the data structure we’ve given it—in this case, in the tibble color_data. Since that tibble has a column called red, eval(my_expr, color_data) gives us that column.

This may not seem life-changing yet, but being able to pass expressions around and evaluate them in contexts of our choosing allows us to seem very clever indeed. For example, let’s create another expression:

add_red_green <- expr(red + green)
typeof(add_red_green)
[1] "language"

The type of add_red_green is language rather than symbol because it contains more than just a symbol, but it’s still an expression, so we can evaluate it in the context of our data frame:

eval(add_red_green, color_data)
[1] 11 22

Still not convinced? Have a look at this function:

run_many_checks <- function(data, ...) {
  conditions <- list(...)
  checks <- vector("list", length(conditions))
  for (i in seq_along(conditions)) {
    checks[[i]] <- eval(conditions[[i]], data)
  }
  checks
}

It takes a tibble and some logical expressions, evaluates each expression in turn, and returns a vector of results:

run_many_checks(color_data, expr(0 < red), expr(red < green))
[[1]]
[1] TRUE TRUE

[[2]]
[1]  TRUE FALSE

We can take it one step further and simply report whether the checks passed or not:

run_all_checks <- function(data, ...) {
  conditions <- list(...)
  checks <- vector("logical", length(conditions))
  for (i in seq_along(conditions)) {
    checks[[i]] <- all(eval(conditions[[i]], data))
  }
  all(checks)
}

run_all_checks(color_data, expr(0 < red), expr(red < green))
[1] FALSE

This is cool, but typing expr(...) over and over is kind of clumsy. It also seems superfluous, since we know that arguments aren’t evaluated before they’re passed into functions. Can we get rid of this and write something that does this?

check_all(color_data, 0 < red, red < green)

The answer is going to be “yes”, but it’s going to take a bit of work.

Square Brackets… Why’d It Have to Be Square Brackets?

Before we go there, a word (or code snippet) of warning. The first version of run_many_checks essentially did this:

conditions <- list(expr(red < green))
eval(conditions[1], color_data)

What I did wrong was use [ instead of [[, which meant that conditions[1] was not an expression—it wa a list containing a single expression. It turns out that evaluating a list containing an expression produces a list of expressions rather than an error, which is so helpful that it only took me an hour to figure out my mistake.

5.5 What is tidy evaluation?

Our goal is to write something that looks like it belongs in the tidyverse. We want to be able to write this:

check_all(color_data, 0 < red, red < green)

without calling expr to quote our expressions explicitly. For simplicity’s sake, our first attempt only handles a single expression:

check_naive <- function(data, test) {
  eval(test, data)
}

When we try it, it fails:

check_naive(color_data, red != green)
Error in eval(test, data): object 'green' not found

This makes sense: by the time we get to the eval call, test refers to a promise that represents the value of red != green in the global environment. Promises are not expressions—each promise contains an expression, but it also contains an environment and a copy of the expression’s value (if it has ever been calculated). As a result, when R sees the call to eval inside check_naive it automatically tries to resolve the promise that contains left != right, and fails because there are no variables with those names in the global environment.

So how can we get the expression out of the promise without triggering evaluation? One way is to use a function called substitute:

check_using_substitute <- function(data, test) {
  subst_test <- substitute(test)
  eval(subst_test, data)
}
check_using_substitute(color_data, red != green)
[1] TRUE TRUE

However, substitute is frowned upon because it does one thing when called interactively on the command line and something else when called inside a function. Instead, we should use a function called enquo from the rlang package. enquo returns an object called a quosure that contains only an unevaluated expression and an environment. Let’s try using that:

check_using_enquo <- function(data, test) {
  q_test <- enquo(test)
  eval(q_test, data)
}
check_using_enquo(color_data, red != green)
<quosure>
expr: ^red != green
env:  global

Ah: a quosure is a structured object, so evaluating it just gives it back to us in the same way that evaluating 2 or "hello" would. What we want to eval is the expression inside the quosure, which we can get using quo_get_expr:

check_using_quo_get_expr <- function(data, test) {
  q_test <- enquo(test)
  eval(quo_get_expr(q_test), data)
}
check_using_quo_get_expr(list(left = 1, right = 2), left != right)
[1] TRUE

All right: we’re ready to write check_all. As a reminder, our test data looks like this:

color_data
# A tibble: 2 x 2
    red green
  <dbl> <dbl>
1     1    10
2    20     2

Our first attempt (which only handles a single test) is going to fail on purpose to demonstrate a common mistake:

check_without_quoting_test <- function(data, test) {
  data %>% transmute(result = test) %>% pull(result) %>% all()
}
check_without_quoting_test(color_data, red < green)
Error: object 'green' not found

That failed because we’re not enquoting the test. Let’s modify it the code to enquote and then pass in the expression:

check_without_quoting_test <- function(data, test) {
  q_test <- enquo(test)
  x_test <- quo_get_expr(q_test)
  data %>% transmute(result = x_test) %>% pull(result) %>% all()
}
check_without_quoting_test(color_data, red < green)
Error: Column `result` is of unsupported type quoted call

Damn—we thought this one had a chance. The problem is that when we say result = x_test, what actually gets passed into transmute is a promise containing an expression. Somehow, we need to prevent R from doing that promise wrapping.

This brings us to enquo’s partner !!, which we can use to splice the expression in a quosure into a function call. !! is pronounced “bang bang” or “oh hell”, depending on how your day is going. It only works in contexts like function calls where R is automatically quoting things for us, but if we use it then, it does exactly what we want:

check_using_bangbang <- function(data, test) {
  q_test <- enquo(test)
  data %>% transmute(result = !!q_test) %>% pull(result) %>% all()
}
check_using_bangbang(color_data, red < green)
[1] FALSE

We are almost in a state of grace. The two rules we must follow are:

  1. Use enquo to enquote every argument that contains an unevaluated expression.
  2. Use !! when passing each of those arguments into a tidyverse function.
check_all <- function(data, ...) {
  tests <- enquos(...)
  result <- TRUE
  for (t in tests) {
    result <- result && (data %>% transmute(result = !!t) %>% pull(result) %>% all())
  }
  result
}

check_all(color_data, 0 < red, red < green)
[1] FALSE

And just to make sure that it succeeds when it’s supposed to:

check_all(color_data, red != green)
[1] TRUE

Backing up a bit, !! works because there are two broad categories of functions in R: evaluating functions and quoting functions. Evaluating functions take arguments as values—they’re what most of us are used to working with. Quoting functions, on the other hand, aren’t passed the values of expressions, but the expressions themselves. When we write color_data$red, the $ function is being passed color_data and the quoted expression red. This is why we can’t use variables as field names with $:

the_string_red <- "red"
color_data$the_string_red
Warning: Unknown or uninitialised column: 'the_string_red'.
NULL

The square bracket operators [ and [[, on the other hand, are evaluating functions, so we can give them a variable containing a column name and get either a single-column tibble:

color_data[the_string_red]     # single square brackets
# A tibble: 2 x 1
    red
  <dbl>
1     1
2    20

or a naked vector:

color_data[[the_string_red]]   # double square brackets
[1]  1 20

5.6 Is it worth it?

Delayed evaluation and quoting are confusing for two reasons:

  1. They expose machinery that most programmers have never had to deal with before (and might not even have known existed). It’s rather like learning to drive an automatic transmission and then switching to a manual one—all of a sudden you have to worry about a gear shift and a clutch.
  2. R’s built-in tools don’t behave as consistently as they could, and the functions provided by the tidverse as alternatives use variations on a small number of names: quo, quote, and enquo might all appear on the same page.

If you would like to know more, or check that what you now think you understand is accurate, this tutorial is a good next step.

5.7 Key Points

  • R uses lazy evaluation: expressions are evaluated when their values are needed, not before.
  • Use expr to create an expression without evaluating it.
  • Use eval to evaluate an expression in the context of some data.
  • Use enquo to create a quosure containing an unevaluated expression and its environment.
  • Use quo_get_expr to get the expression out of a quosure.
  • Use !! to splice the expression in a quosure into a function call.

References

Wickham, Hadley. 2019. Advanced R. 2nd ed. Chapman; Hall/CRC.