Почему 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