Что делают новые аккуратные скобки eval {{}}? Передача аргументов между функциями и использование as_label - PullRequest
1 голос
/ 01 августа 2020

Недавно пытаясь исправить проблему с передачей аргументов из функции на основе dplyr в функцию ggplot, я с удивлением обнаружил, что существует совершенно новое программирование с dplyr виньетка и соответствующая версия для ggplot2 в пакетах . Я надеялся убить двух зайцев одним выстрелом: изучить новые заклинания tidyeval и избавиться от своей проблемы.

Мне нужна была функция для создания пользовательских графиков, которые иногда могут быть вызваны другой функцией, которая выполняет некоторые предварительные операции. -обработка предоставленных данных; но это было не так:

library(ggplot2)
library(dplyr)

my_plot <- function(df, x_var, colour_var = "cyl") {
  
  char_col <- df %>%
    pull(colour_var) %>%
    is.character()
  
  if(!char_col ) cat(colour_var, "is not character.")
  
  
  ggplot(df) +
    geom_point(aes(x = !! ensym(x_var), y = hp, colour = !!ensym(colour_var))) +
    labs(title = paste("Passed in x variable:", x_var))
  
}

process_n_plot <- function(x_var, val, colour_var) {

  
  cat("You are filtering variable", x_var, "\n")
  
  mtcars %>% 
    filter(!!ensym(x_var)  > val) %>% 
    my_plot(x_var = x_var, colour_var = colour_var)
  
  
}

process_n_plot("disp", 200, "cyl")
#> You are filtering variable "disp" 
#> cyl is not character.


my_plot(mtcars, "disp", "cyl")
#> cyl is not character.

I realise I could have just used aes_string... But I was actually operating in ggraph and forgot 'cause I'd never used aes_string there. Also, having strings as arguments was me assuming it would the most straightforward way, but still preferred to call the functions with unquoted variable names.

So, things worked when calling my_plot directly; and almost worked when called "indirectly". The vignettes didn't quite cover these use cases, so I had to test.

However, replacing !!ensym(x_var) with {{x_var}} above does not work; neither does the naïve approach below with bare variable names. {{}} as per the vignettes seems to combine the steps of enquo(s) and !!(!) but that poses a problem when trying to use something like as_label/as_string, which want you to enquo but not !!.

library(ggplot2)
library(dplyr)
library(rlang)

my_plot <- function(df, x_var, colour_var = "cyl") {
  
  discrete_col <- df %>%
    pull(colour_var) %>%
    is.character()
  
  if(!discrete_col) cat(colour_var, "is not character.")
  
  
  ggplot(df) +
    geom_point(aes(x = {{ x_var }}, y = hp, colour = {{ colour_var }})) +
    labs(title = paste("Passed in x variable:", as_label({{ x_var }})))
  
}

process_n_plot <- function(x_var, val, colour_var) {

  
  cat("You are filtering variable", as_label( {{ x_var }} ), "\n")
  
  mtcars %>% 
    filter({{ x_var }}  > val) %>% 
    my_plot(x_var = x_var, colour_var = colour_var)
  
  
}

process_n_plot(disp, 200, cyl)
#> Error in is_quosure(quo): object 'disp' not found

my_plot(mtcars, disp, cyl)
#> Error: object 'cyl' not found

Обратите внимание, что удаление labs делает my_plot работает нормально, как и ожидалось из виньетки.

Ответы [ 2 ]

3 голосов
/ 01 августа 2020

В ожидании, пока настоящие эксперты объяснят некоторые кровавые детали, решение выглядит следующим образом:

library(ggplot2)
library(dplyr)
library(rlang)

my_plot <- function(df, x_var, colour_var = cyl) {
  
  char_col <- df %>%
    pull({{colour_var}}) %>%
    is.character()
  
  if(!char_col) cat(as_label( enquo(colour_var)), "is not character.")
  
  
  ggplot(df) +
    geom_point(aes(x = {{ x_var }}, y = hp, colour = {{ colour_var }})) +
    labs(title = paste("Passed in x variable:", as_label( enquo(x_var))))
  
}

process_n_plot <- function(x_var, val, colour_var) {

  
  cat("You are filtering variable", as_label( enquo(x_var) ), "\n")
  
  mtcars %>% 
    filter({{ x_var }}  > val) %>% 
    my_plot(x_var = {{ x_var }}, colour_var = {{ colour_var }})
  
  
}

process_n_plot(disp, 200, cyl)
#> You are filtering variable disp 
#> cyl is not character.


my_plot(mtcars, disp, cyl)
#> cyl is not character.

В конце концов, вы можете enquo, чтобы as_label работал и, что удивительно, {{}} все равно знал, что делать. То же самое применимо, если вы используете ensym и as_string.

Вы также можете просто сделать:

geom_point(aes(x = {{ x_var }}  , y = hp, colour = {{colour_var}})) +
labs(title = paste("Passed in x variable:", as_label( enquo(x_var) )))

Использование {{passing_arg}} (то же, что и использование !!enquo(passing_arg)) решает проблему передачи аргументов между функциями.

Что касается того, почему исходная функция работала только для colour_var, по причинам, которые я до сих пор не совсем понимаю, ensym() интерпретировал обещание аргумента x_var как простую строку . После того, как вы преобразовали обещание, например, используя его в другом вызове функции, как я сделал с colour_var, оно сработало.

1 голос
/ 01 августа 2020

Вы в основном правы, синтаксис {{ }} - это ярлык на !!enquo(). Использование {{}} предполагает, что вам никогда не понадобится промежуточное вычисленное выражение. Поскольку вы хотите вызвать as_label, то этот ярлык вам не подходит, потому что в этом случае вам нужно неоцененное выражение.

Также обратите внимание, что enquo и ensym ведут себя по-разному при передаче строки. ensym() превратит это значение в символ, а enquo() сохранит его как символьный литерал

f1 <- function(x) rlang::qq_show(!!enquo(x))
f2 <- function(x) rlang::qq_show(!!ensym(x))
f1("hello")
# ^"hello"
f2("hello")
# hello

, что также влияет на то, как они превращаются в метки

g1 <- function(x) as_label(enquo(x))
g2 <- function(x) as_label(ensym(x))
g1("hello")
# [1] "\"hello\""
g1(hello)
# [1] "hello"
g2("hello")
# [1] "hello"
g2(hello)
# [1] "hello"

При работе в tidyverse важно следить за тем, какие функции нуждаются в символах / выражениях, которые должны быть введены в вызов, каким функциям нужны сами невычисленные символы / выражения, а какие могут принимать необработанные символьные значения. быть внимательным при передаче значений другим функциям. Обычно функции смотрят только на имена переменных, переданные им напрямую. Если вы хотите передавать значения «насквозь», не оценивая их, вам нужно будет использовать !! или {{}}, если на другой стороне есть функция rlang, чтобы разобраться в этом.

foo <- function(x) {
  ensym(x)
}
a1 <- function(x) {
  foo(x)
}
a2 <- function(x) {
  foo({{x}})
}
a3 <- function(x) {
  foo(!!ensym(x))
}
a1(test)
# x
a2(test)
# test
a3(test)
# test

Посмотрите, как только последние два смотрят "полностью вверх", чтобы найти test

Есть три способа захвата парматеров с помощью rlang: quo (quosure), expr (выражение) и sym (символ). sym() или символ - это просто имя одной переменной или столбца. expr() или выражение может быть переменной с вызовами функций или операторами, включающими другие переменные. Примеры: x+y или foo(x). А quo() или quosure - это выражение, которое также отслеживает, где оно было определено, на случай, если ему нужно найти значения для любой из переданных вами переменных. Знание того, какая из них подходит для вашего конкретного варианта использования, также может сделать большая разница.

...