在 rlang::expr 中取消引用循环变量

Unquoting the loop variable in `rlang::expr`

我在使用运算符 !! 迭代构造涉及循环变量的表达式时遇到意外行为。

从循环变量本身创建一个表达式,例如,

X <- list()
for( i in 1:3 )
  X[[i]] <- rlang::expr( !!i )
str(X)
# List of 3
#  $ : int 1
#  $ : int 2
#  $ : int 3

按预期工作。但是,试图构造任何涉及循环变量的复杂表达式,例如

Y <- list()
for( i in 1:3 )
    Y[[i]] <- rlang::expr( 1 + (!!i) )
str(Y)
# List of 3
#  $ : language 1 + 3L
#  $ : language 1 + 3L
#  $ : language 1 + 3L

似乎生成了包含循环变量最终值的表达式。这几乎就像表达式在循环期间被捕获为 quote( 1 + (!!i) ),但是通过 !! 取消引用仅在整个循环执行后发生,而不是在每次迭代时发生。

有人可以解释一下这种行为吗?这是一个错误吗?预期目标是通过循环捕获未评估的表达式 1+11+21+3。因此 Y 的预期输出将是

# List of 3
#  $ : language 1 + 1L
#  $ : language 1 + 2L
#  $ : language 1 + 3L

旁注:!!i 周围的额外括号是为了解决 .

使用 rlang 0.2.1.

我不是 100% 确定我理解正确。您是否可能在 rlang::eval_tidy 之后计算表达式?

X <- list()
for( i in 1:3 )
    X[[i]] <- rlang::eval_tidy(rlang::expr(!!i + 1))
#[[1]]
#[1] 2
#
#[[2]]
#[1] 3
#
#[[3]]
#[1] 4

似乎 for 创造了某种环境/框架,在这个环境/框架中评估发生在最后(对于最后一个 i)。一种可能的处理方法是避免 for 并使用 purrr::lapply.

Y <- purrr::map(1:3, ~ rlang::expr( 1 + (!! .) )) 
str(Y)
#> List of 3
#>  $ : language 1 + 1L
#>  $ : language 1 + 2L
#>  $ : language 1 + 3L

Y <- lapply(1:3, function(i) rlang::expr( 1 + (!! i) )) 
str(Y)
#> List of 3
#>  $ : language 1 + 1L
#>  $ : language 1 + 2L
#>  $ : language 1 + 3L

while 在控制台中工作,但在与 reprex 一起使用时失败(未显示)。

Y <- list()
i <- 1L
while (i <= 3) {
    Y[[i]] <- rlang::expr( 1 + (!!i) )
    i <- i + 1L
}
str(Y)
#> List of 3
#>  $ : language 1 + 1L
#>  $ : language 1 + 2L
#>  $ : language 1 + 3L

不完全确定是否是这种情况,但我认为这与惰性评估有关。主要思想是 R 在不使用时不计算表达式。在您的示例中, rlang::expr( !!i ) 是 "self-evaluating" 因为表示常量的表达式是常量本身。这会导致在 for 循环的每次迭代中计算以下内容:

X <- list()
for( i in 1:3 )
  X[[i]] <- rlang::expr( !!i )

注意X的元素不再是表达式而是整数:

> str(X)
List of 3
 $ : int 1
 $ : int 2
 $ : int 3

identical(rlang::expr(1),1)
# [1] TRUE

然而,你的第二个例子有 rlang::expr( 1 + (!!i) ),它在 for 循环的每次迭代中仍然是一个表达式,没有评估。惰性计算导致 R 仅在循环结束时计算 i,它采用 i 的最后一个值。解决这个问题的方法是 force 评估 i:

Y <- list()
for( i in 1:3 ){
  force(i)
  Y[[i]] <- rlang::expr( 1 + (!!i) )
}

> str(Y)
List of 3
 $ : language 1 + 1L
 $ : language 1 + 2L
 $ : language 1 + 3L

请注意,懒惰求值过去也会影响 lapply 等函数,如本问题所述:Explain a lazy evaluation quirk,但此问题已在 R 3.2.0 中得到修复。 lapply 等高阶函数现在将参数强制传递给内部函数。请参阅@jhin 在同一问题中的回答。这就是为什么@Mike Badescu 的 lapply 解决方案现在有效。