You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

核心原因解析

  1. {{}}的实际行为:{{x}}作为!!enquo(x)的语法糖,仅当x是函数的参数时才成立。在递归场景中,lhs_first_arg是从已有表达式中提取的裸语法对象,此时{{lhs_first_arg}}会直接将这个裸表达式注入到调用中,等价于把表达式原样传递给下一次%+%,而非对它执行enquo()引用操作。

  2. !!enquo(lhs_first_arg)的错误逻辑:enquo()的作用是捕获函数参数的引用(包含环境信息),但lhs_first_arg已经是提取后的表达式,不是当前函数的参数。此时enquo(lhs_first_arg)会生成一个引用lhs_first_arg变量本身的quosure,而非你需要的原始表达式。当你用!!解引用这个quosure时,由于当前调用%+%并非准引用上下文,就会触发报错。

  3. 无enquo()版本的合理性:递归调用时直接传递裸表达式,让lhs参数接收原始表达式而非quosure;仅在顶层的非递归调用中使用enquo()捕获参数,跳过了递归时不必要的引用操作,完全匹配"提取首尾项相加"的需求。


内容的提问来源于stack exchange,提问作者Ethan.S

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 23:35:56