Почему 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, такой как replace + eval, или мне действительно нужно научиться любить все эти rlang функции, потому что есть ли в этом польза (скорость, четкость, композиционность, ...)?


person mbiron    schedule 06.04.2018    source источник
comment
Я думаю, что мир был бы лучше, если бы dplyr:: ppl просто позволял нам передавать имена переменных в виде символьных строк, как в старых подчеркнутых вариантах, таких как mutate_(). imo, еще лучшим вариантом было бы иметь аргумент типа colnames_as_strings=TRUE для mutate() и др ..., который упростил бы использование dplyr как в интерактивном режиме, так и в программном обеспечении. А пока добро пожаловать в _5 _ / _ 6_ ад ...   -  person lefft    schedule 07.04.2018
comment
tl; dr: стратегия enquo() действительно имеет смысл только в том случае, если вы глубоко привержены возможности передавать имена столбцов без кавычек (мне непонятно, почему это важно, но да ладно). может быть, есть какая-то фундаментальная причина, которая требует понимания внутреннего устройства dplyr, чтобы понять ...   -  person lefft    schedule 07.04.2018
comment
@lefft Мне сказали, что передача имен столбцов в виде символов «опасна и ненадежна», но я так и не получил убедительного объяснения, почему это происходит, за исключением случаев, которые кажутся мне странно редкими. Я полагаю, что если вы регулярно сталкиваетесь с этими крайними случаями, это имеет больше смысла, это просто странно для меня, потому что я никогда не думал, что когда-либо сталкивался.   -  person joran    schedule 07.04.2018
comment
@joran да, я могу представить, что если смешивать стандартную и нестандартную оценку, могут возникнуть проблемы - но я полностью согласен, я по-прежнему не убежден в этом. опасный и ненадежный бит (на самом деле я бы сказал, что передача имен без кавычек более опасна + ненадежна, как с base::subset()!)   -  person lefft    schedule 07.04.2018
comment
@lefft Нет, это дерьмо. На самом деле это ничего не решает и ничего не упрощает. Кроме того, найдите строчно набранный текст. Вы предлагаете разрушить систему шрифтов. Это априори плохая идея.   -  person Konrad Rudolph    schedule 07.04.2018
comment
@KonradRudolph Я предлагаю разрешить выбор / подмножество на основе символов на языке, определение которого использует это соглашение ...   -  person lefft    schedule 07.04.2018
comment
@KonradRudolph Единственное, что я чувствую достаточно хорошо осведомленным, чтобы прокомментировать этот момент, - это то, что вашему делу, возможно, не помогает это первое предложение.   -  person joran    schedule 07.04.2018
comment
@lefft Вы предлагаете разрешить строки вместо переменных внутри выражений (или выражения внутри строк? Это еще хуже). Это важное различие. Никто не говорит о простом выборе столбцов.   -  person Konrad Rudolph    schedule 07.04.2018
comment
хорошо, последняя мысль: мотивация возникает из-за невозможности передать вектор символов в group_by(), select() и mutate_at()/summarize_at(). Когда имена столбцов неизвестны (или не могут быть известны) заранее, может быть сложно написать хорошие функции разделения-применения-объединения в dplyr. Иногда даже легче использовать base::tapply(), именно потому, что вы можете указать группирующие столбцы как символьные строки, которые вы передаете в качестве параметра ... В конкретном случае, показанном OP, было бы, конечно, ужасно, если бы "m" означало mydata$m (или всякий раз, когда имя столбца используется в правой части = внутри функции таблицы dplyr).   -  person lefft    schedule 07.04.2018
comment
(fwiw, я люблю dplyr:: и использую его каждый день - я просто хочу, чтобы он был как можно лучше!)   -  person lefft    schedule 07.04.2018
comment
@lefft Нет, это вообще не проблема. Просто используйте group_by(data, !! var). Честно говоря, я не вижу трудностей. Это простая, понятная, последовательная, но мощная абстракция. Таким образом, это диаметрально противоположно тому, что предлагают tapply и т. Д.   -  person Konrad Rudolph    schedule 07.04.2018
comment
@joran Меня одолело раздражение. Но ваш комментарий иллюстрирует постоянную проблему в этой дискуссии: люди уделяют исключительное внимание тону, а не содержанию. Факты не имеют значения. Я мог бы попытаться использовать другие слова, но это ничего не изменит: комментарий с технически плохим (опробовано, протестировано и нашло нужным) решением получил много голосов. Мой комментарий, который, помимо нецензурной лексики, содержал указатели и фактические аргументы против, был проигнорирован.   -  person Konrad Rudolph    schedule 07.04.2018
comment
@KonradRudolph fwiw Я верю тебе (хотя бы по той причине, что я знаю, что ты знаешь об этом намного больше, чем я). Я просто пытался сдвинуть тон в другом направлении.   -  person joran    schedule 07.04.2018


Ответы (4)


Я хочу дать ответ, не зависящий от 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
}
person Artem Sokolov    schedule 08.11.2018
comment
Отличный ответ, чувак, это было то, что я искал. Большое спасибо. - person mbiron; 10.11.2018

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

person Tung    schedule 06.04.2018
comment
Спасибо! Было бы неплохо увидеть, может ли replace + eval работать и в этих случаях. В конце концов, мой вопрос был в основном: могу ли я уйти от программирования с использованием функций dplyr NSE с помощью старого доброго заменителя + eval, или мне действительно нужно научиться любить все те rlang функции, которые вы упомянули, потому что в этом есть польза ? - person mbiron; 07.04.2018
comment
@mbiron: Мне любопытно увидеть решение, использующее substitute+eval. ИМО, если вы используете много tidyverse пакетов, то стоит узнать о tidyeval, поскольку Хэдли и другие разработчики продвигаются в этом направлении. Вот пример разбора входных строк в dplyr. Другой пример использования tidyeval в ggplot2 - person Tung; 07.04.2018
comment
@mbiron Конечно, теоретически здесь можно использовать eval и substitute. Но решения будут мучительно сложными и сложными. Вклад {rlang} заключается в обобщении, формализации и упрощении решения, опираясь на существующие исследования в области информатики. - person Konrad Rudolph; 07.04.2018

Представьте, что вы хотите умножить другой 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 - с заменой вы можете легко получить только второй способ.

person Neal Fultz    schedule 06.04.2018
comment
Понятно. Кажется, это связано с чем-то, что отображается в это сообщение в блоге: !! лучше описывает композицию функций, использующих NSE. Тем не менее, примеры кажутся немного неудобными. - person mbiron; 07.04.2018

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

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

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

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

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

решение случая Артема с базой 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

решение 1-го случая Тунга с базой 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

person Moody_Mudskipper    schedule 04.10.2019
comment
Конечно, мы могли бы использовать и *_at() варианты, но это не главное здесь - person Moody_Mudskipper; 04.10.2019
comment
Очень умное использование eval.parent()! - person Artem Sokolov; 04.10.2019