Почему enquo +!! предпочтительнее заменить + eval
В следующем примере, почему мы должны использовать f1
над f2
? В каком-то смысле это более эффективно? Для кого-то, использовавшего основу R, кажется более естественным использовать опцию "подставить + 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
функции, потому что есть польза (скорость, ясность, композиционность,...)?
3 ответа
Я хочу дать ответ, который не зависит от 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.
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
Представьте, что есть другой х, который вы хотите умножить:
> 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-й путь.
Чтобы добавить нюанса, эти вещи не обязательно так сложны в базовом 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