rlang中`{{}}`与`!!enquo()`的差异及递归报错问题解析
{{}}和!!enquo()在递归运算符实现中表现不一致? 我在编写一个仅对求和式最左侧和最右侧项进行相加的%+%运算符玩具版本时,遇到了{{}}表现符合预期,但!!rlang::enquo()触发报错的情况。rlang文档提到{{}}本质上是enquo()和!!的合并操作,但该场景下二者表现截然不同。同时,一个递归时不使用enquo()的版本也能正常运行,这背后的原因是什么?
一、使用{{}}的正常实现
这个版本能正确提取首尾项相加:
library(rlang) # 使用`{{}}`实现仅首尾相加的求和 `%+%` <- function(lhs, rhs) { # 引用`lhs`以检查 lhs_expr <- rlang::quo_get_expr(rlang::enquo(lhs)) lhs_call <- lhs_expr[[1]] # 基准情况 if (lhs_call != as_name("%+%")) { lhs <- eval(lhs) # 若`lhs`仍是调用则求值 return(lhs + rhs) } # 递归:将下一个`%+%`的左侧作为新的`lhs` lhs_first_arg <- lhs_expr[[2]] {{ lhs_first_arg }} %+% rhs } 1 %+% 1 #> [1] 2 1 %+% 2 %+% 3 %+% 4 # 结果应为5 #> [1] 5 mean(c(1, 2, 3)) %+% "A" %+% "B" %+% -10 #> [1] -8
二、使用!!enquo()的报错实现
替换为!!enquo()后,递归调用会触发错误:
library(rlang) # 使用`!!enquo()`实现仅首尾相加的求和 `%+%` <- function(lhs, rhs) { # 引用`lhs`以检查 lhs_expr <- rlang::quo_get_expr(rlang::enquo(lhs)) lhs_call <- lhs_expr[[1]] # 基准情况 if (lhs_call != as_name("%+%")) { lhs <- eval(lhs) # 若`lhs`仍是调用则求值 return(lhs + rhs) } # 递归:将下一个`%+%`的左侧作为新的`lhs` lhs_first_arg <- lhs_expr[[2]] (!!enquo(lhs_first_arg)) %+% rhs } 1 %+% 1 #> [1] 2 try(1 %+% 2 %+% 3 %+% 4) #> Error in eval(lhs) : #> Quosures can only be unquoted within a quasiquotation context. #> #> # Bad: list(!!myquosure) #> #> # Good: dplyr::mutate(data, !!myquosure)
三、不使用enquo()的递归实现
这个版本在递归时直接传递裸表达式,同样能正常运行:
library(rlang) # 仅首尾相加的求和,递归场景不引用`lhs` `%+%` <- function(lhs, rhs, fn = rlang::caller_fn()) { # 引用`lhs`以检查 if (!identical(fn, `%+%`)) { lhs_expr <- rlang::quo_get_expr(rlang::enquo(lhs)) } else { lhs_expr <- lhs } lhs_call <- lhs_expr[[1]] # 基准情况 if (lhs_call != as_name("%+%")) { lhs <- eval(lhs) # 若`lhs`仍是调用则求值 return(lhs + rhs) } # 递归:将下一个`%+%`的左侧作为新的`lhs` lhs_first_arg <- lhs_expr[[2]] lhs_first_arg %+% rhs } 1 %+% 1 #> [1] 2 1 %+% 2 %+% 3 %+% 4 # 结果应为5 #> [1] 5 mean(c(1, 2, 3)) %+% "A" %+% "B" %+% -10 #> [1] -8
核心原因解析
{{}}的实际行为:{{x}}作为!!enquo(x)的语法糖,仅当x是函数的参数时才成立。在递归场景中,lhs_first_arg是从已有表达式中提取的裸语法对象,此时{{lhs_first_arg}}会直接将这个裸表达式注入到调用中,等价于把表达式原样传递给下一次%+%,而非对它执行enquo()引用操作。!!enquo(lhs_first_arg)的错误逻辑:enquo()的作用是捕获函数参数的引用(包含环境信息),但lhs_first_arg已经是提取后的表达式,不是当前函数的参数。此时enquo(lhs_first_arg)会生成一个引用lhs_first_arg变量本身的quosure,而非你需要的原始表达式。当你用!!解引用这个quosure时,由于当前调用%+%并非准引用上下文,就会触发报错。无
enquo()版本的合理性:递归调用时直接传递裸表达式,让lhs参数接收原始表达式而非quosure;仅在顶层的非递归调用中使用enquo()捕获参数,跳过了递归时不必要的引用操作,完全匹配"提取首尾项相加"的需求。
内容的提问来源于stack exchange,提问作者Ethan.S

