I am aware of the other answers on substitute, quote, and eval (e.g.,
this
answer).
However, I am still confused about the following scenario. I will err on the
site of verbosity for the steps below to ensure what I am trying to do is
clear.
Suppose I have four functions (i.e., see below) that all take an expression, pass it around, and only the root one evaluates it.
f1 <- function(expr) {
x <- "Hello!"
do.call(eval, list(expr = expr))
}
f2 <- function(expr) {
f1(expr)
}
f3 <- function(expr) {
f2(expr)
}
f4 <- function(expr) {
f3(expr)
}
Then, I am interested in passing an unquoted expression to any of f2,
f3, or f4 that should not be evaluated right away. More specifically, I
would like to call:
f4(print(paste0("f4: ", x)))
f3(print(paste0("f3: ", x)))
f2(print(paste0("f2: ", x)))
and observe the following output:
[1] "f4: Hello!"
[1] "f3: Hello!"
[1] "f2: Hello!"
However, calling, e.g., f4(print(paste0("f4: ", x))), right now will result in
an error that x is not defined, which is, indeed, not defined until the environment of f1:
Error in paste0("x: ", x) : object 'x' not found
I can use substitute in f4 to get the parse tree of the expression, e.g.:
f4 <- function(expr) {
f3(substitute(expr))
}
then call the function
f4(print(paste0("f4: ", x)))
and obtain:
[1] "f4: Hello!"
[1] "f4: Hello!"
The double output is probably because the argument of eval, i.e., expr
in f1, is unquoted. Quoting it solves this early evaluation:
f1 <- function(expr) {
x <- "Hello!"
do.call(eval, list(expr = quote(expr)))
}
f4(print(paste0("f4: ", x)))
# [1] "f4: Hello!"
If I apply the same logic to, say, f3, e.g.:
f3 <- function(expr) {
f2(substitute(expr))
}
calling f3 works as expected, i.e.:
f3(print(paste0("f3: ", x)))
# [1] "f3: Hello!"
but now calling f4 fails, with the output being expr.
f4(print(paste0("f4: ", x)))
At this point, I am not exactly sure how or if this is even possible to achieve.
Of course, the simplest way would be to just pass a quoted expression to any of these functions. However, I am curious how to achieve this without quote.
I think it is easier if we leave the
print()statement and focus on passing quoted expressions within nested functions.We need to do two things. First, capture the expression in each function. In base R we do this with
substitute().Then we need to evaluate the captured expression before passing it to the next function (in which it will be captured again).
In
f1we do not have this problem, since it is the last function in our chain.In
f2things get more tricky. Usingeval()insidef1will capture that as well, so we need to evaluate early. In base R we can do this withbquote()to create a call and use.()insidebquote()to evaluate an expression early. So we wrapf1()intoeval(bquote())and evaluate the captured expression early with.(cap_expr). We need the outereval()becausebquote()returns an unevaluated call.The 'rlang' package offers a different set of tools which makes things a bit easier.
Here we can capture an expression inside a function with
enexpr(), and evaluate it witheval_tidy().eval_tidy()supports argument splicing with the bang bang operator!!which allows us to evaluate the captured expressioncapt_exprearly.Note that, normally when programming with 'rlang' we would use
enquo()to capture an expression and its environment (called a quosure). Buteval_tidy()evaluates this quosure in the environment in which it was captured, and therexdoesn't exist. So we would need to change the environment or add the function environment to the caller environment as parent. Either way, in this case just capturing the expression withenexpr()is easier than usingenquo().Created on 2023-02-23 by the reprex package (v2.0.1)