19.3 Quoting

The first part of quasiquotation is quotation: capturing an expression without evaluating it. We’ll need a pair of functions because the expression can be supplied directly or indirectly, via lazily-evaluated function argument. I’ll start with the rlang quoting functions, then circle back to those provided by base R.

19.3.1 Capturing expressions

There are four important quoting functions. For interactive exploration, the most important is expr(), which captures its argument exactly as provided:

expr(x + y)
#> x + y
expr(1 / 2 / 3)
#> 1/2/3

(Remember that white space and comments are not part of the expression, so will not be captured by a quoting function.)

expr() is great for interactive exploration, because it captures what you, the developer, typed. It’s not so useful inside a function:

f1 <- function(x) expr(x)
f1(a + b + c)
#> x

We need another function to solve this problem: enexpr(). This captures what the caller supplied to the function by looking at the internal promise object that powers lazy evaluation (Section 6.5.1).

f2 <- function(x) enexpr(x)
f2(a + b + c)
#> a + b + c

(It’s called “en”-expr() by analogy to enrich. Enriching someone makes them richer; enexpr()ing a argument makes it an expression.)

To capture all arguments in ..., use enexprs().

f <- function(...) enexprs(...)
f(x = 1, y = 10 * z)
#> $x
#> [1] 1
#> $y
#> 10 * z

Finally, exprs() is useful interactively to make a list of expressions:

exprs(x = x ^ 2, y = y ^ 3, z = z ^ 4)
# shorthand for
# list(x = expr(x ^ 2), y = expr(y ^ 3), z = expr(z ^ 4))

In short, use enexpr() and enexprs() to capture the expressions supplied as arguments by the user. Use expr() and exprs() to capture expressions that you supply.

19.3.2 Capturing symbols

Sometimes you only want to allow the user to specify a variable name, not an arbitrary expression. In this case, you can use ensym() or ensyms(). These are variants of enexpr() and enexprs() that check the captured expression is either symbol or a string (which is converted to a symbol67). ensym() and ensyms() throw an error if given anything else.

f <- function(...) ensyms(...)
#> [[1]]
#> x
#> [[1]]
#> x

19.3.3 With base R

Each rlang function described above has an equivalent in base R. Their primary difference is that the base equivalents do not support unquoting (which we’ll talk about very soon). This make them quoting functions, rather than quasiquoting functions.

The base equivalent of expr() is quote():

quote(x + y)
#> x + y

The base function closest to enexpr() is substitute():

f3 <- function(x) substitute(x)
f3(x + y)
#> x + y

The base equivalent to exprs() is alist():

alist(x = 1, y = x + 2)
#> $x
#> [1] 1
#> $y
#> x + 2

The equivalent to enexprs() is an undocumented feature of substitute()68:

f <- function(...) as.list(substitute(...()))
f(x = 1, y = 10 * z)
#> $x
#> [1] 1
#> $y
#> 10 * z

There are two other important base quoting functions that we’ll cover elsewhere:

  • bquote() provides a limited form of quasiquotation, and is discussed in Section 19.5.

  • ~, the formula, is a quoting function that also captures the environment. It’s the inspiration for quosures, the topic of the next chapter, and is discussed in Section 20.3.4.

19.3.4 Substitution

You’ll most often see substitute() used to capture unevaluated arguments. However, as well as quoting, substitute() also does substitution (as its name suggests!). If you give it an expression, rather than a symbol, it will substitute in the values of symbols defined in the current environment.

f4 <- function(x) substitute(x * 2)
f4(a + b + c)
#> (a + b + c) * 2

I think this makes code hard to understand, because if it is taken out of context, you can’t tell if the goal of substitute(x + y) is to replace x, y, or both. If you do want to use substitute() for substitution, I recommend that you use the second argument to make your goal clear:

substitute(x * y * z, list(x = 10, y = quote(a + b)))
#> 10 * (a + b) * z

19.3.5 Summary

When quoting (i.e. capturing code), there are two important distinctions:

  • Is it supplied by the developer of the code or the user of the code? In other words, is it fixed (supplied in the body of the function) or varying (supplied via an argument)?

  • Do you want to capture a single expression or multiple expressions?

This leads to a 2 \(\times\) 2 table of functions for rlang, Table 19.1, and for base R, Table 19.2.

Table 19.1: rlang quasiquoting functions
Developer User
One expr() enexpr()
Many exprs() enexprs()
Table 19.2: base R quoting functions
Developer User
One quote() substitute()
Many alist() as.list(substitute(...()))

19.3.6 Exercises

  1. How is expr() implemented? Look at its source code.

  2. Compare and contrast the following two functions. Can you predict the output before running them?

    f1 <- function(x, y) {
      exprs(x = x, y = y)
    f2 <- function(x, y) {
      enexprs(x = x, y = y)
    f1(a + b, c + d)
    f2(a + b, c + d)
  3. What happens if you try to use enexpr() with an expression (i.e.  enexpr(x + y) ? What happens if enexpr() is passed a missing argument?

  4. How are exprs(a) and exprs(a = ) different? Think about both the input and the output.

  5. What are other differences between exprs() and alist()? Read the documentation for the named arguments of exprs() to find out.

  6. The documentation for substitute() says:

    Substitution takes place by examining each component of the parse tree as follows:

    • If it is not a bound symbol in env, it is unchanged.
    • If it is a promise object (i.e., a formal argument to a function) the expression slot of the promise replaces the symbol.
    • If it is an ordinary variable, its value is substituted, unless env is .GlobalEnv in which case the symbol is left unchanged.

    Create examples that illustrate each of the above cases.

  1. This is for compatibility with base R, which allows you to provide a string instead of a symbol in many places: "x" <- 1, "foo"(x, y), c("x" = 1).↩︎

  2. Discovered by Peter Meilstrup and described in R-devel on 2018-08-13.↩︎