【问题标题】:Why is enquo + !! preferable to substitute + eval为什么是 enquo + !!最好替代 + eval
【发布时间】:2018-09-16 23:04:19
【问题描述】:

在下面的例子中,为什么我们应该使用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

换句话说,除了这个特定的例子之外,我的问题是:我是否可以使用 dplyr NSE 函数和像替代 + eval 这样的良好基础 R 来摆脱编程,或者我真的需要学会爱吗?所有这些rlang 函数,因为它有好处(速度、清晰度、组合性……)?

【问题讨论】:

  • 我认为如果dplyr:: ppl 只允许我们将变量名作为字符串传递,就像在像@这样的旧下划线变体中一样,世界会变得更美好987654327@。 imo,一个更好的选择是为mutate() 等提供一个像colnames_as_strings=TRUE 这样的参数......这将使在交互和软件中使用dplyr 变得简单。但在那之前,欢迎来到enquo()/!!地狱......
  • @lefft 有人告诉我,将列名作为字符传递是“危险且不可靠的”,但我从来没有得到令人信服的解释来解释为什么会这样,除非在我看来非常罕见的情况下.我想如果你经常遇到这些边缘情况会更有意义,这对我来说很奇怪,因为我认为我从来没有遇到过。
  • @KonradRudolph 我建议在定义使用该约定的语言中允许基于字符的选择/子集...
  • @KonradRudolph 在这一点上,我唯一觉得有足够知识可以评论的是,第一句话可能对您的情况没有帮助。
  • @KonradRudolph fwiw 我相信你(如果没有其他原因,我知道你比我知道的更多)。我只是想把语气调到不同的方向。

标签: r dplyr nse tidyeval


【解决方案1】:

我想给出一个独立于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】:

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

信用:Programming with dplyr

【讨论】:

  • 谢谢!不过,很高兴看到替代+评估是否也可以在这些情况下工作。最后,我的问题基本上是:我可以使用 dplyr NSE 函数和良好的 ol'替代 + eval 进行编程,还是我真的需要学会爱你提到的所有 rlang 函数,因为有一个好处给它?
  • @mbiron:我也很想看到使用substitute+eval 的解决方案。 IMO 如果您使用大量 tidyverse 软件包,那么值得了解 tidyeval,因为 Hadley 和其他开发人员正在朝着这个方向努力。 Here 是将输入字符串解析为 dplyr 的示例。 Another exampleggplot2 中使用tidyeval
  • @mbiron 当然,理论上你可以在这里使用evalsubstitute。但解决方案将非常复杂和复杂。 {rlang} 的贡献是通过在现有计算机科学研究的基础上推广、形式化和简化解决方案。
【解决方案3】:

想象有一个不同的 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 让您可以更好地控制范围 - 使用替代品您只能轻松获得第二种方式。

【讨论】:

  • 我明白了。它似乎与出现在this blog post 中的东西有关:!! 更好地处理使用 NSE 的函数组合。不过,这些例子似乎有点尴尬
【解决方案4】:

为了增加一些细微差别,这些东西在基础 R 中不一定那么复杂。

重要的是要记住在正确的环境中评估替换参数时使用eval.parent(),如果您正确使用eval.parent(),嵌套调用中的表达式将找到它们的方式。如果你不这样做,你可能会发现环境地狱:)。

我使用的基本工具箱由quote()substitute()bquote()as.call()do.call() 组成(后者在与substitute() 一起使用时很有用

这里不详述如何在base 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() 变体,但这不是重点
  • 非常巧妙地使用eval.parent()
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-07-21
  • 2020-03-04
  • 1970-01-01
  • 1970-01-01
  • 2012-03-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多