为什么在R语言中要优先使用enquo() + !!,而不是substitute() + eval()?

37
在下面的示例中,为什么我们应该更喜欢使用f1而不是f2?在某种意义上,它是否更有效率?对于习惯于基础R的人来说,使用“substitute + eval”选项似乎更自然。
为什么我们应该更喜欢使用f1而不是f2?在某种意义上,它是否更有效率?对于习惯于基础R的人来说,使用“substitute + eval”选项似乎更自然。
library(dplyr)

d = data.frame(x = 1:5,
               y = rnorm(5))

# using enquo + !!
f1 = function(mydata, myvar) {
  m = enquo(myvar)
  mydata %>%
    mutate(two_y = 2 * !!m)
}

# using substitute + eval    
f2 = function(mydata, myvar) {
  m = substitute(myvar)
  mydata %>%
    mutate(two_y = 2 * eval(m))
}

all.equal(d %>% f1(y), d %>% f2(y)) # TRUE

换句话说,在这个特定的例子之外,我的问题是:我能否使用旧版R中的substitute+eval来编程,并使用dplyr NSE函数,还是我真的需要学会喜欢那些 rlang 函数,因为它有好处(速度、清晰度、组合性等)?


15
如果 dplyr:: 的人能够允许我们传递变量名称作为字符字符串,就像旧的带下划线的变体 mutate_() 一样,我认为世界会变得更美好。在我看来,一个更好的选择是在 mutate() 等函数中增加一个类似于 colnames_as_strings=TRUE 的参数... 这将使得在交互式和软件中使用 dplyr 变得简单明了。但在那之前,欢迎来到 enquo()/!! 地狱... - lefft
4
我听说将列名以字符形式传递是“危险和不可靠”的���但我从未得到令人信服的解释,除非是在我看来非常罕见的情况下。我想,如果你经常遇到这些边缘案例,这样做可能更有意义,但对我来说很奇怪,因为我认为我从来没有遇到过这种情况。 - joran
2
@KonradRudolph 我建议在使用该约定的语言中允许基于字符的选择/子集。 - lefft
6
@KonradRudolph 我现在唯一感到足够有发言权的事情是,你的案例可能并没有因为第一句话而得到帮助。 - joran
3
@KonradRudolph,就我所知,您比我更了解这个问题,因此我相信您的观点(即使没有其他原因)。我只是试图将语气引向不同的方向。 - joran
显示剩余8条评论
4个回答

22

我希望给出一个与 dplyr 无关的答案,因为使用 enquo 相比于 substitute 有非常明显的优势。两者都会在函数所处的调用环境中查找给定给函数的表达式。区别在于:substitute() 只查找一次,而 !!enquo() 会正确地遍历整个调用堆栈。

考虑一个使用 substitute() 的简单函数:

f <- function( myExpr ) {
  eval( substitute(myExpr), list(a=2, b=3) )
}

f(a+b)   # 5
f(a*b)   # 6

当调用嵌套在另一个函数中时,此功能会中断:

g <- function( myExpr ) {
  val <- f( substitute(myExpr) )
  ## Do some stuff
  val
}

g(a+b)
# myExpr     <-- OOPS

现在考虑使用enquo()重写相同函数:

library( rlang )

f2 <- function( myExpr ) {
  eval_tidy( enquo(myExpr), list(a=2, b=3) )
}

g2 <- function( myExpr ) {
  val <- f2( !!enquo(myExpr) )
  val
}

g2( a+b )    # 5
g2( b/a )    # 1.5

这就是为什么enquo()+ !!substitute()+ eval()更可取。 dplyr充分利用了这个特性来构建一组连贯的NSE函数。

更新:rlang 0.4.0引入了一个新操作符{{(读作"curly curly"),它有效地简化了!!enquo()的定义。 这使我们可以将g2的定义简化为

g2 <- function( myExpr ) {
  val <- f2( {{myExpr}} )
  val
}

2
太棒了,伙计!这正是我在寻找的答案。非常感谢你。 - mbiron

6

enquo()!!也允许您使用其他dplyr动词进行编程,如group_byselect。我不确定substituteeval是否可以做到这一点。请看以下示例,我稍微修改了您的数据框:

library(dplyr)

set.seed(1234)
d = data.frame(x = c(1, 1, 2, 2, 3),
               y = rnorm(5),
               z = runif(5))

# select, group_by & create a new output name based on input supplied
my_summarise <- function(df, group_var, select_var) {

  group_var <- enquo(group_var)
  select_var <- enquo(select_var)

  # create new name
  mean_name <- paste0("mean_", quo_name(select_var))

  df %>%
    select(!!select_var, !!group_var) %>% 
    group_by(!!group_var) %>%
    summarise(!!mean_name := mean(!!select_var))
}

my_summarise(d, x, z)

# A tibble: 3 x 2
      x mean_z
  <dbl>  <dbl>
1    1.  0.619
2    2.  0.603
3    3.  0.292

编辑:同时,enquos!!!使得捕获变量列表更加容易。

# example
grouping_vars <- quos(x, y)
d %>%
  group_by(!!!grouping_vars) %>%
  summarise(mean_z = mean(z))

# A tibble: 5 x 3
# Groups:   x [?]
      x      y mean_z
  <dbl>  <dbl>  <dbl>
1    1. -1.21   0.694
2    1.  0.277  0.545
3    2. -2.35   0.923
4    2.  1.08   0.283
5    3.  0.429  0.292


# in a function
my_summarise2 <- function(df, select_var, ...) {

  group_var <- enquos(...)
  select_var <- enquo(select_var)

  # create new name
  mean_name <- paste0("mean_", quo_name(select_var))

  df %>%
    select(!!select_var, !!!group_var) %>% 
    group_by(!!!group_var) %>%
    summarise(!!mean_name := mean(!!select_var))
}

my_summarise2(d, z, x, y)

# A tibble: 5 x 3
# Groups:   x [?]
      x      y mean_z
  <dbl>  <dbl>  <dbl>
1    1. -1.21   0.694
2    1.  0.277  0.545
3    2. -2.35   0.923
4    2.  1.08   0.283
5    3.  0.429  0.292

来源: 使用 dplyr 进行编程


1
谢谢!如果substitute+eval在这些情况下也能起作用,那就太好了。最终,我的问题基本上是:我能否使用dplyr NSE函数进行编程,并使用好老式的substitute+eval,还是我真的需要学会喜欢你提到的所有“rlang”函数,因为这样做有好处? - mbiron
1
@mbiron:我很想看到使用substitute+eval的解决方案。在我看来,如果你正在使用许多tidyverse包,那么学习tidyeval是值得的,因为Hadley和其他开发人员正在朝着这个方向推进。这里是一个将输入字符串解析为dplyr的示例。另一个示例ggplot2中使用了tidyeval - Tung
2
@mbiron 当然你在理论上可以使用 evalsubstitute。但是解决方案将会非常复杂和混乱。{rlang} 的贡献在于通过建立在现有计算机科学研究的基础上,将解决方案进行了概括、形式化和简化。 - Konrad Rudolph

5

想象一下,有一个不同的 x 你想要相乘:

> x <- 3
> f1(d, !!x)
  x            y two_y
1 1 -2.488894875     6
2 2 -1.133517746     6
3 3 -1.024834108     6
4 4  0.730537366     6
5 5 -1.325431756     6

没有使用!!的vs:

> f1(d, x)
  x            y two_y
1 1 -2.488894875     2
2 2 -1.133517746     4
3 3 -1.024834108     6
4 4  0.730537366     8
5 5 -1.325431756    10

!!substitute提供更多的控制范围-使用substitute只能轻松获得第2种方式。


我明白了。这似乎与此博客文章中出现的某些内容有关:!!更好地处理使用NSE的函数组合。不过,这些示例看起来有点尴尬。 - mbiron

4
为了增加一些细节,这些东西在基础R中并不一定很复杂。
重要的是要记住在适当时使用`eval.parent()`来在正确的环境中评估替换参数,如果您正确地使用`eval.parent()`,嵌套调用中的表达式将会找到它们的方式。如果您没有这样做,您可能会发现自己陷入环境困境中 :).
我使用的基本工具箱包括`quote()`、`substitute()`、`bquote()`、`as.call()`和`do.call()`(后者与`substitute()`结合使用时非常有用)。
在不详细说明的情况下,以下是如何在基础R中解决@Artem和@Tung提出的问题,而不使用任何整洁的评估方法,以及最后一个例子,不使用quo/enquo,但仍从拼接和取消引用(!!!和!!)中受益。
我们将看到拼接和取消引用使代码更美观(但需要函数支持!),在当前情况下使用quosures并没有显着改善情况(但仍然可以争论)。

使用基础R解决Artem的问题

f0 <- function( myExpr ) {
  eval(substitute(myExpr), list(a=2, b=3))
}

g0 <- function( myExpr ) {
  val <- eval.parent(substitute(f0(myExpr)))
  val
}

f0(a+b)
#> [1] 5
g0(a+b)
#> [1] 5

使用基础 R 解决 Tung 的第一个案例

my_summarise0 <- function(df, group_var, select_var) {

  group_var  <- substitute(group_var)
  select_var <- substitute(select_var)

  # create new name
  mean_name <- paste0("mean_", as.character(select_var))

  eval.parent(substitute(
  df %>%
    select(select_var, group_var) %>% 
    group_by(group_var) %>%
    summarise(mean_name := mean(select_var))))
}

library(dplyr)
set.seed(1234)
d = data.frame(x = c(1, 1, 2, 2, 3),
               y = rnorm(5),
               z = runif(5))
my_summarise0(d, x, z)
#> # A tibble: 3 x 2
#>       x mean_z
#>   <dbl>  <dbl>
#> 1     1  0.619
#> 2     2  0.603
#> 3     3  0.292

使用基础R解决Tung的第二个案例
grouping_vars <- c(quote(x), quote(y))
eval(as.call(c(quote(group_by), quote(d), grouping_vars))) %>%
  summarise(mean_z = mean(z))
#> # A tibble: 5 x 3
#> # Groups:   x [3]
#>       x      y mean_z
#>   <dbl>  <dbl>  <dbl>
#> 1     1 -1.21   0.694
#> 2     1  0.277  0.545
#> 3     2 -2.35   0.923
#> 4     2  1.08   0.283
#> 5     3  0.429  0.292

在一个函数中:
my_summarise02 <- function(df, select_var, ...) {

  group_var  <- eval(substitute(alist(...)))
  select_var <- substitute(select_var)

  # create new name
  mean_name <- paste0("mean_", as.character(select_var))

  df %>%
    {eval(as.call(c(quote(select),quote(.), select_var, group_var)))} %>% 
    {eval(as.call(c(quote(group_by),quote(.), group_var)))} %>%
    {eval(bquote(summarise(.,.(mean_name) := mean(.(select_var)))))}
}

my_summarise02(d, z, x, y)
#> # A tibble: 5 x 3
#> # Groups:   x [3]
#>       x      y mean_z
#>   <dbl>  <dbl>  <dbl>
#> 1     1 -1.21   0.694
#> 2     1  0.277  0.545
#> 3     2 -2.35   0.923
#> 4     2  1.08   0.283
#> 5     3  0.429  0.292


使用基本的 R 代码,但是利用 !! 和 !!! 来解决 Tung 的第二个案例问题。
grouping_vars <- c(quote(x), quote(y))

d %>%
  group_by(!!!grouping_vars) %>%
  summarise(mean_z = mean(z))
#> # A tibble: 5 x 3
#> # Groups:   x [3]
#>       x      y mean_z
#>   <dbl>  <dbl>  <dbl>
#> 1     1 -1.21   0.694
#> 2     1  0.277  0.545
#> 3     2 -2.35   0.923
#> 4     2  1.08   0.283
#> 5     3  0.429  0.292

在一个函数中:

my_summarise03 <- function(df, select_var, ...) {

  group_var  <- eval(substitute(alist(...)))
  select_var <- substitute(select_var)

  # create new name
  mean_name <- paste0("mean_", as.character(select_var))

  df %>%
    select(!!select_var, !!!group_var) %>% 
    group_by(!!!group_var) %>%
    summarise(.,!!mean_name := mean(!!select_var))
}

my_summarise03(d, z, x, y)
#> # A tibble: 5 x 3
#> # Groups:   x [3]
#>       x      y mean_z
#>   <dbl>  <dbl>  <dbl>
#> 1     1 -1.21   0.694
#> 2     1  0.277  0.545
#> 3     2 -2.35   0.923
#> 4     2  1.08   0.283
#> 5     3  0.429  0.292


当然,我们也可以使用*_at()变量,但这并不是重点。 - moodymudskipper
1
非常聪明地使用了 eval.parent() - Artem Sokolov

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