在具有公式界面的函数中使用准引号
using quasiquotation in functions with formula interface
我想编写一个自定义函数,它可以接受 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
))
)
}
裸
适用于有和没有公式界面的函数
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, "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)
如何修改原始函数,使其适用于上述所有输入方式以及所用的两种函数?
rlang
的妙处在于,您可以控制何时希望通过 !!
和 {{}}
运算符计算值。您似乎想要创建一个函数,该函数在同一个参数中接受字符串、符号和(可能已求值的)表达式。 ensym
使用符号或裸字符串实际上很容易,但也希望允许像 colnames(mtcars)[9]
这样的代码在 return 之前必须 evaluated字符串是问题所在。这可能会非常混乱。例如,当您 运行 以下内容时,您期望的行为是什么?
am <- 'disp'
cyl <- 'gear'
foo(mtcars, am, cyl)
如果你想假设所有 "calls" 都应该被评估但符号和文字不应该被评估,你可以编写一个辅助函数。这是一个 "cleaner" 函数
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
))
)
}
这样做将使所有这些return具有相同的值
foo(mtcars, am, cyl)
foo(mtcars, "am", "cyl")
foo(mtcars, colnames(mtcars)[9], colnames(mtcars)[2])
但你可能只是在拖延其他可能的问题。我不建议用这种代码过度解释用户意图。这就是为什么最好明确允许他们自己逃脱。也许提供两个不同版本的函数,可以与需要评估的参数和不需要评估的参数一起使用。
我必须同意@MrFlick 和其他人关于混合标准和非标准评估时固有的歧义。 (我也在你的 similar question from a while ago 中指出了这一点。)
然而,有人可能会争辩说 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] )
我想编写一个自定义函数,它可以接受 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
))
)
}
裸
适用于有和没有公式界面的函数
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, "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)
如何修改原始函数,使其适用于上述所有输入方式以及所用的两种函数?
rlang
的妙处在于,您可以控制何时希望通过 !!
和 {{}}
运算符计算值。您似乎想要创建一个函数,该函数在同一个参数中接受字符串、符号和(可能已求值的)表达式。 ensym
使用符号或裸字符串实际上很容易,但也希望允许像 colnames(mtcars)[9]
这样的代码在 return 之前必须 evaluated字符串是问题所在。这可能会非常混乱。例如,当您 运行 以下内容时,您期望的行为是什么?
am <- 'disp'
cyl <- 'gear'
foo(mtcars, am, cyl)
如果你想假设所有 "calls" 都应该被评估但符号和文字不应该被评估,你可以编写一个辅助函数。这是一个 "cleaner" 函数
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
))
)
}
这样做将使所有这些return具有相同的值
foo(mtcars, am, cyl)
foo(mtcars, "am", "cyl")
foo(mtcars, colnames(mtcars)[9], colnames(mtcars)[2])
但你可能只是在拖延其他可能的问题。我不建议用这种代码过度解释用户意图。这就是为什么最好明确允许他们自己逃脱。也许提供两个不同版本的函数,可以与需要评估的参数和不需要评估的参数一起使用。
我必须同意@MrFlick 和其他人关于混合标准和非标准评估时固有的歧义。 (我也在你的 similar question from a while ago 中指出了这一点。)
然而,有人可能会争辩说 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] )