如何在R中使用`substitute`和`quote`来嵌套函数?

4

我知道其他有关substitutequoteeval的答案(例如这个答案)。然而,我仍然对以下情况感到困惑。为了确保我正在尝试做的事情清楚,我会在下面的步骤中错误地使用冗长的方式。

假设我有四个函数(即下面看到的),它们都接受一个expression,将其传递,并且只有根函数评估它。

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)
}

接下来,我对于将一个未加引号的表达式传递给f2f3或者f4并且不希望立即执行它感兴趣。更具体地说,我想要调用:

f4(print(paste0("f4: ", x)))
f3(print(paste0("f3: ", x)))
f2(print(paste0("f2: ", x)))

请观察以下输出结果:
[1] "f4: Hello!"
[1] "f3: Hello!"
[1] "f2: Hello!"

然而,现在调用例如f4(print(paste0("f4: ", x)))会导致错误,因为x未被定义,直到f1的环境中才被定义:

Error in paste0("x: ", x) : object 'x' not found

我可以在 f4 中使用 substitute 来获取表达式的解析树,例如:

f4 <- function(expr) {
    f3(substitute(expr))
}

然后调用该函数。
f4(print(paste0("f4: ", x)))

并获得:

[1] "f4: Hello!"
[1] "f4: Hello!"

双重输出可能是因为 f1 中的参数 expr 没有加引号导致早期计算。添加引号即可解决此问题:eval
f1 <- function(expr) {
    x <- "Hello!"
    do.call(eval, list(expr = quote(expr)))
}

f4(print(paste0("f4: ", x)))
# [1] "f4: Hello!"

如果我将同样的逻辑应用于,比如说,f3,例如:
f3 <- function(expr) {
    f2(substitute(expr))
}

调用f3的效果与预期相符,即:

f3(print(paste0("f3: ", x)))
# [1] "f3: Hello!"

但是现在调用 f4 失败了,输出的结果是 expr
f4(print(paste0("f4: ", x)))

目前,我并不确定如何实现这一点,甚至不确定是否可能实现。 当然,最简单的方法是将带引号的表达式传递给任何一个这些函数。但是,我想知道如何在没有quote的情况下实现这一点。


你为什么要使用 print()?不添加这个额外的层,示例会更或多或少相同吗?还是你想让函数正确处理 print() 语句? - TimTeaFan
@TimTeaFan,“print()”本质上并不是必需的——我只是用它来表示传递的表达式是否有任何意外的早期计算。我想没有“print()”也可以。 - Mihai
1个回答

3

我认为如果我们不使用print()语句,而是专注于在嵌套函数中传递带引号的表达式会更容易。

我们需要做两件事情。首先,在每个函数中捕获表达式。在基本R中,我们可以使用substitute()来实现。

然后,我们需要在将其传递给下一个函数之前评估捕获的表达式(其中它将再次被捕获)。

f1中,我们没有这个问题,因为它是我们链中的最后一个函数。

f2中,情况变得更加棘手。在f1中使用eval()也会将其捕获,因此我们需要提前评估。在基本R中,我们可以使用bquote()创建调用,并在bquote()内部使用.()提前评估表达式。因此,我们将f1()包装在eval(bquote())中,并使用.(cap_expr)提前评估捕获的表达式。我们需要外层的eval(),因为bquote()返回一个未评估的调用。

f1 <- function(expr) {
  cap_expr <- substitute(expr)
  x <- "Hello!"
  eval(cap_expr)
}

f2 <- function(expr) {
  cap_expr <- substitute(expr)
  eval(bquote(f1(.(cap_expr))))
}

f3 <- function(expr) {
  cap_expr <- substitute(expr)
  eval(bquote(f2(.(cap_expr))))
}

f4 <- function(expr) {
  cap_expr <- substitute(expr)
  eval(bquote(f3(.(cap_expr))))
}

f1(paste("f1:", x))
#> [1] "f1: Hello!"
f2(paste("f2:", x))
#> [1] "f2: Hello!"
f3(paste("f3:", x))
#> [1] "f3: Hello!"
f4(paste("f4:", x))
#> [1] "f4: Hello!"

'rlang'包提供了一组不同的工具,使事情变得更容易。

在这里,我们可以使用enexpr()在函数内捕获表达式,并使用eval_tidy()进行评估。 eval_tidy()支持使用双感叹号运算符!!进行参数拼接,这使我们可以尽早地评估捕获的表达式capt_expr

请注意,通常在使用'rlang'进行编程时,我们会使用enquo()来捕获表达式及其环境(称为quosure)。但是,eval_tidy()在捕获它的环境中评估此quosure,在那里x不存在。因此,我们需要更改环境或将函数环境添加到调用者环境作为父级。无论哪种方式,在这种情况下,仅使用enexpr()捕获表达式比使用enquo()更容易。

library(rlang)

f1 <- function(expr) {
  cap_expr <- enexpr(expr)
  x <- "Hello!"
  eval_tidy(cap_expr)
}

f2 <- function(expr) {
  cap_expr <- enexpr(expr)
  eval_tidy(f1(!!cap_expr))
}

f3 <- function(expr) {
  cap_expr <- enexpr(expr)
  eval_tidy(f2(!!cap_expr))
}

f4 <- function(expr) {
  cap_expr <- enexpr(expr)
  eval_tidy(f3(!!cap_expr))
}

f1(paste("f1:", x))
#> [1] "f1: Hello!"
f2(paste("f2:", x))
#> [1] "f2: Hello!"
f3(paste("f3:", x))
#> [1] "f3: Hello!"
f4(paste("f4:", x))
#> [1] "f4: Hello!"

reprex包(v2.0.1)于2023年2月23日创建


1
现在这个意思清楚多了。我很感激 rlang 的添加! - Mihai
1
是的,rlang语法更容易理解。它也有助于理解我们在基本R中正在做什么。基本上是相同的方法,只是在基本R中更加复杂。 - TimTeaFan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接