我想编写一个自定义函数,它可以接受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]
这样的代码是问题所在。这可能会令人困惑。例如,运行以下命令时预期的行为是什么?
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])
但您可能只是在延迟可能的其他问题。我不建议用这种代码过度解释用户意图。这就是为什么最好明确地允许他们自己无法逃避。也许提供两个不同版本的函数,可以与需要评估的参数和不需要的参数一起使用。
我必须同意@MrFlick和其他人关于混合标准和非标准评估时固有的歧义。(我在你刚才的类似问题中也指出了这一点。
但是,有人可能会争辩说,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] )