替换函数(例如names<-
)在像 那样调用时似乎不使用惰性求值names(x) <- c("a", "b")
。
为了演示,让我们定义一个函数来获取数字的小数部分和相应的替换函数 - 但在替换函数内部,包含一行来打印解除约束的value
参数。
fractional <- function(x) {
x %% 1
}
`fractional<-` <- function(x, value) {
print(rlang::enexpr(value))
invisible(x %/% 1 + value)
}
现在如果我们fractional<-
直接调用,它会打印我们给出的表达式value
:
x <- 10.1
`fractional<-`(x, 0.2 + 0.2)
#> 0.2 + 0.2
但是如果我们以赋值形式调用它,它会打印表达式的求值结果:
x <- 10.1
fractional(x) <- 0.2 + 0.2
#> [1] 0.4
语言定义解释了替换函数,例如:
names(x) <- c("a","b")
相当于
`*tmp*` <- x x <- "names<-"(`*tmp*`, value=c("a","b")) rm(`*tmp*`)
但这并不能重现这种行为:
x <- 10.1
`*tmp*` <- x
x <- "fractional<-"(`*tmp*`, value=0.2 + 0.2)
rm(`*tmp*`)
#> 0.2 + 0.2
其中内部发生了什么事情<-
使得它在value
评估后被传递,有什么方法可以规避这种行为?fractional<-
编辑: @SamR 指出使用substitute
捕获了承诺中的表达式:
x <- 10.1
`fractional<-` <- function(x, value) {
print(substitute(value))
invisible(x %/% 1 + value)
}
fractional(x) <- 0.2 + 0.2
#> 0.2 + 0.2
因此,显然我错误地认为value
在传递给之前对进行了评估fractional<-
。但是,我仍然非常想知道为什么 base::substitute
在这里按预期工作,而 rlang::enexpr
和朋友却没有。毕竟,在内部enexpr
使用substitute
:
enexpr <- function(arg) {
.Call(ffi_enexpr, substitute(arg), parent.frame())
}
在 R Studio 中进行调试表明,无论是以赋值形式调用fractional(x) <- 0.2 + 0.2
还是以前缀形式调用时"fractional<-"(x, 0.2 + 0.2)
,fractional<-
都会传递一个未评估的承诺value
:
当以前缀形式调用时,它仍未被计算:
但是在以赋值形式调用时,会在调用之后进行评估enexpr
:
我想知道这是否与在赋值形式中该函数由原始函数调用有关<-
?但不清楚为什么会有所不同。
在 R 中,形式
被称为复杂赋值,这个术语也适用于各种子集赋值,例如:
R 解释器通过文件中的applydefine函数处理底层 C 代码中的复杂分配
src/main/eval.c
。如果不知道 R 是如何用 C 实现的,那么这段代码很难理解,但本质上,当解析器遇到 时
f(x) <- y
,它会在求值之前重新排列表达式。首先,它<-
在函数名末尾附加一个f
,并构建对函数 的调用`f<-`
。但是,它并不是简单地重新排列符号并调用。相反,它用临时变量代替,用承诺对象代替 来`f<-`(x, y)
调用。我们将在下面看到这样做的充分理由。`f<-`
x
y
在开始之前,让我们先确认一下直接调用函数和通过复杂赋值调用函数之间的区别。我们可以编写一个函数,它只打印调用它的参数,然后保持
x
原样:直接调用这个,我们不会感到惊讶:
但是看看当我们使用复杂的赋值语法时会发生什么:
我们可以看到,
x
已被一个名为的变量所取代*tmp*
,并且"foo"
已被一个承诺对象所取代。在 C 代码中,该
*tmp*
变量被称为R_TmpvalSymbol
,并被写入调用中,以便在嵌套复杂赋值的情况下保存中间计算。代码注释中对此进行了描述:至于使用承诺来代替右边的表达式,代码注释解释说
但是,R 喜欢使用惰性求值(不到必要的时候才求值代码),C 函数不会完全求值右侧,而是
applydefine
将值锁定为承诺。承诺对象有两个组成部分:首先,一些存储为语言对象的代码,其次是求值该代码的环境。使用承诺执行几乎任何操作都会强制求值。虽然看起来复杂赋值没有使用惰性求值,但实际上确实如此 - 只是我们需要在求值之前从承诺中提取语言对象。幸运的是,正如 SamR 在评论中提到的那样,如果你给它一个承诺,它就会这样做(如果你感兴趣,它就在这里
substitute
做)enexpr
这里不起作用的原因是,value
在复杂赋值中传递给的参数不是未求值的语言对象0.2 + 0.2
,而是一个由语言对象和求值环境组成的承诺。因此,从内部调用的 C 函数0.2 + 0.2
返回的是这个承诺对象,而不是原始代码。返回承诺会强制求值,因此我们得到的是数值 0.4,而不是语言对象。rlang:::ffi_enexpr
enexpr
0.2 + 0.2