Почему enquo + !! предпочтительнее заменить + 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

Другими словами, помимо этого конкретного примера, мой вопрос: могу ли я уйти с программированием, используя функции dplyr NSE с хорошей базой R, подобной замене + eval, или мне действительно нужно научиться любить все эти функции rlang потому что есть польза для него (скорость, ясность, композиционность,...)?

Ответ 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 ввел новый оператор {{ (произносится "вьющиеся кудрявые"), который фактически является короткой рукой для !!enquo(). Это позволяет нам упростить определение g2 до

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

Ответ 2

enquo() и !! также позволяет вам программировать другие глаголы dplyr такие как group_by и select. Я не уверен, что substitute и eval могут это сделать. Взгляните на этот пример, когда я немного модифицирую фрейм данных

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

Ответ 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

против без !! :

> 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 - с заменой вы можете получить только 2-й путь.

Ответ 4

Чтобы добавить некоторый нюанс, эти вещи не обязательно настолько сложны в базе R.

Важно не забывать использовать eval.parent(), когда это уместно, для оценки замещенных аргументов в правильной среде, если вы правильно используете eval.parent(), выражение во вложенных вызовах найдет свои пути. Если вы этого не сделаете, вы можете обнаружить ад окружающей среды :).

Ящик базового инструмента, который я использую, состоит из quote(), substitute(), bquote(), as.call() и do.call() (последний полезен при использовании с substitute()

Не вдаваясь в детали, мы расскажем, как решить в базе R случаи, представленные @Artem и @Tung, без какой-либо аккуратной оценки, а затем последний пример, не использующий quo/enquo, но все же извлекающий выгоду из сплайсинга и удаления кавычек. (!!! и !!)

Мы увидим, что сплайсинг и расстановка кавычек делают код более приятным (но требуют функций для его поддержки!), И что в настоящих случаях использование кавычек не приводит к значительным улучшениям (но, тем не менее, это возможно).

решение дела Артема с базой R

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

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

решение Тунга 2-го случая с базой R

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

решение 2-го случая Тунга с базой R, но с использованием !! и !!!

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