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:
function(x) expr(x)
f1 <-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).
function(x) enexpr(x)
f2 <-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()
.
function(...) enexprs(...)
f <-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.
function(...) ensyms(...)
f <-f(x)
#> [[1]]
#> x
f("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()
:
function(x) substitute(x)
f3 <-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:
function(...) as.list(substitute(...()))
f <-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:
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.
function(x) substitute(x * 2)
f4 <-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.
Developer | User | |
---|---|---|
One | expr() |
enexpr() |
Many | exprs() |
enexprs() |
Developer | User | |
---|---|---|
One | quote() |
substitute() |
Many | alist() |
as.list(substitute(...())) |
19.3.6 Exercises
How is
expr()
implemented? Look at its source code.Compare and contrast the following two functions. Can you predict the output before running them?
function(x, y) { f1 <-exprs(x = x, y = y) } function(x, y) { f2 <-enexprs(x = x, y = y) }f1(a + b, c + d) f2(a + b, c + d)
What happens if you try to use
enexpr()
with an expression (i.e.enexpr(x + y)
? What happens ifenexpr()
is passed a missing argument?How are
exprs(a)
andexprs(a = )
different? Think about both the input and the output.What are other differences between
exprs()
andalist()
? Read the documentation for the named arguments ofexprs()
to find out.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.
- If it is not a bound symbol in
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)
.↩︎Discovered by Peter Meilstrup and described in R-devel on 2018-08-13.↩︎