использование квазиквотирования в функциях с интерфейсом формулы - PullRequest
0 голосов
/ 05 февраля 2020

Я хочу написать пользовательскую функцию, которая может принимать входные данные bare и "string" и обрабатывать обе функции как с интерфейсом формулы, так и без него.

пример пользовательской функции

# setup
set.seed(123)
library(tidyverse)

# custom function
foo <- function(data, x, y) {
  # function without formula
  print(table(data %>% dplyr::pull({{ x }}), data %>% dplyr::pull({{ y }})))

  # function with formula
  print(
    broom::tidy(stats::t.test(
      formula = rlang::new_formula({{ rlang::ensym(y) }}, {{ rlang::ensym(x) }}),
      data = data
    ))
  )
}

bare

работает для обеих функций с и без интерфейса формулы

foo(mtcars, am, cyl)
#>    
#>      4  6  8
#>   0  3  4 12
#>   1  8  3  2

#> # A tibble: 1 x 10
#>   estimate estimate1 estimate2 statistic p.value parameter conf.low conf.high
#>      <dbl>     <dbl>     <dbl>     <dbl>   <dbl>     <dbl>    <dbl>     <dbl>
#> 1     1.87      6.95      5.08      3.35 0.00246      25.9    0.724      3.02
#> # ... with 2 more variables: method <chr>, alternative <chr>

string

работает как для функций с интерфейсом формул, так и без него

foo(mtcars, "am", "cyl")
#>    
#>      4  6  8
#>   0  3  4 12
#>   1  8  3  2

#> # A tibble: 1 x 10
#>   estimate estimate1 estimate2 statistic p.value parameter conf.low conf.high
#>      <dbl>     <dbl>     <dbl>     <dbl>   <dbl>     <dbl>    <dbl>     <dbl>
#> 1     1.87      6.95      5.08      3.35 0.00246      25.9    0.724      3.02
#> # ... with 2 more variables: method <chr>, alternative <chr>

в качестве имен столбцов

работает только для функций без интерфейса формул

foo(mtcars, colnames(mtcars)[9], colnames(mtcars)[2])
#>    
#>      4  6  8
#>   0  3  4 12
#>   1  8  3  2

#> Error: Only strings can be converted to symbols
#> Backtrace:
#>     x
#>  1. \-global::foo(mtcars, colnames(mtcars)[9], colnames(mtcars)[2])
#>  2.   +-base::print(...)
#>  3.   +-broom::tidy(...)
#>  4.   +-stats::t.test(...)
#>  5.   +-rlang::new_formula(...)
#>  6.   \-rlang::ensym(y)

Как изменить исходную функцию так что он будет работать со всеми вышеупомянутыми способами ввода входов и для обоих видов используемых функций?

Ответы [ 2 ]

3 голосов
/ 06 февраля 2020

Я должен согласиться с @MrFlick и другими в отношении неоднозначности, присущей смешиванию стандартной и нестандартной оценки. (Я также указывал на это в вашем подобном вопросе некоторое время go.)

Однако можно утверждать, что dplyr::select() работает с символами, строками и выражениями вида colnames(.)[.]. Если вам абсолютно необходим один и тот же интерфейс, вы можете использовать tidyselect для разрешения ввода:

library( rlang )
library( tidyselect )

ttest <- function(data, x, y) {
  ## Identify locations of x and y in data, get column names as symbols
  s <- eval_select( expr(c({{x}},{{y}})), data ) %>% names %>% syms

  ## Use the corresponding symbols to build the formula by hand
  broom::tidy(stats::t.test(
    formula = new_formula( s[[2]], s[[1]] ),
    data = data
  ))
}

## All three now work
ttest( mtcars, am, cyl )
ttest( mtcars, "am", "cyl" )
ttest( mtcars, colnames(mtcars)[9], colnames(mtcars)[2] )
3 голосов
/ 06 февраля 2020

Хорошая философия rlang заключается в том, что вы можете контролировать, когда хотите, чтобы значения оценивались с помощью операторов !! и {{}}. Кажется, вы хотите создать функцию, которая принимает строки, символы и (возможно, вычисляемые) выражения в одном и том же параметре. Использовать символы или пустые строки на самом деле легко с ensym, но также требуется разрешить код, подобный colnames(mtcars)[9], который должен быть evaulated , прежде чем возвращать строку - проблема. Это может быть довольно запутанным. Например, какое поведение вы ожидаете, когда запускаете следующее?

am <- 'disp'
cyl <- 'gear'
foo(mtcars, am, cyl)

Вы можете написать вспомогательную функцию, если хотите предположить, что все «вызовы» должны оцениваться, а символы и литералы - нет. Вот «более чистая» функция

clean_quo <- function(x) {
  if (rlang::quo_is_call(x)) {
    x <- rlang::eval_tidy(x)
  } else if (!rlang::quo_is_symbolic(x)) {
    x <- rlang::quo_get_expr(x)
  }
  if (is.character(x)) x <- rlang::sym(x)
  if (!rlang::is_quosure(x)) x <- rlang::new_quosure(x)
  x
}

, и вы можете использовать ее в своей функции с

foo <- function(data, x, y) {
  x <- clean_quo(rlang::enquo(x))
  y <- clean_quo(rlang::enquo(y))

  # function without formula
  print(table(data %>% dplyr::pull(!!x), data %>% dplyr::pull(!!y)))

  # function with formula
  print(
    broom::tidy(stats::t.test(
      formula = rlang::new_formula(rlang::quo_get_expr(y), rlang::quo_get_expr(x)),
      data = data
    ))
  )
}

Это позволит всем этим возвращать одинаковые значения

foo(mtcars, am, cyl)
foo(mtcars, "am", "cyl")
foo(mtcars, colnames(mtcars)[9], colnames(mtcars)[2])

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

...