在自定义 dplyr 包装函数中使用带引号的变量

Using quoted variables in a custom dplyr wrapper function

我的问题如下。我有一个在 dplyr::mutate 中运行的函数 foo。此函数接受 tidyselect 语法。我想构建一个包装函数 bar ,它也应该支持 tidyselect 语法。我正在寻找一种干净的方法来将 tidyselected 列从 bar 传递到 foo。听起来很简单,但问题是 foo 需要接受将被引用的裸用户输入,它还需要接受来自包装函数的已经引用的列。

那么让我们看看问题所在:

library(dplyr)

myiris <- as_tibble(iris)

# this is a minimal function supporting tidyselect
# its a toy function, which just returns the tidyselected columns 

foo <- function(cols){
  data <- cur_data()
  vars <- tidyselect::eval_select(rlang::enquo(cols),  data)
  out <- data[, vars]
  
  names(out) <- paste0("new_", names(out))
  out
}

# the function is working:
myiris %>%
  mutate(foo(c(Sepal.Length)))
#> # A tibble: 150 x 6
#>    Sepal.Length Sepal.Width Petal.Length Petal.Width Species new_Sepal.Length
#>           <dbl>       <dbl>        <dbl>       <dbl> <fct>              <dbl>
#>  1          5.1         3.5          1.4         0.2 setosa               5.1
#>  2          4.9         3            1.4         0.2 setosa               4.9
#>  3          4.7         3.2          1.3         0.2 setosa               4.7
#>  4          4.6         3.1          1.5         0.2 setosa               4.6
#>  5          5           3.6          1.4         0.2 setosa               5  
#>  6          5.4         3.9          1.7         0.4 setosa               5.4
#>  7          4.6         3.4          1.4         0.3 setosa               4.6
#>  8          5           3.4          1.5         0.2 setosa               5  
#>  9          4.4         2.9          1.4         0.2 setosa               4.4
#> 10          4.9         3.1          1.5         0.1 setosa               4.9
#> # … with 140 more rows

# this is a wrapper function around `foo`
bar <- function(df, .cols) {
  .cols <- rlang::enquo(.cols)
  mutate(df, foo(.cols))
}

# this will throw an error
myiris %>%
  bar(Sepal.Length)

#> Note: Using an external vector in selections is ambiguous.
#> ℹ Use `all_of(.cols)` instead of `.cols` to silence this message.
#> ℹ See <https://tidyselect.r-lib.org/reference/faq-external-vector.html>.
#> This message is displayed once per session.
#> Error: Problem with `mutate()` input `..1`.
#> x Must subset columns with a valid subscript vector.
#> x Subscript has the wrong type `quosure/formula`.
#> ℹ It must be numeric or character.
#> ℹ Input `..1` is `foo(.cols)`.

reprex package (v0.3.0)

于 2021-04-14 创建

上面的方法行不通,这是完全有道理的。对我来说不明显的是如何以干净一致的方式处理这个问题。

下面我展示了我的尝试以及我想出了什么样的平庸的解决方法。

我想我能做的是:检查列是否已经被引用,如果没有,enquote。然而,这似乎是不可能的。一旦未引用的列用于任何类型的操作,它们将被评估和更改。 enquo 必须作为第一件事发生。但如果它先发生,我无法检查它们是否已被引用。

# we would need to check in foo
# if cols is already quoted or not
# but this seems not to be possible
# since `cols` changes, once it is used / touched

foo <- function(cols){
  data <- cur_data()
  if (!rlang::is_quosure(cols)) {
    cols <- enquo(cols)
  }
  vars <- tidyselect::eval_select(cols, data)
  out <- data[, vars]
  
  names(out) <- paste0("new_", names(out))
  out
}

# not working
iris %>%
  mutate(foo(c(Sepal.Length)))
#> Error: Problem with `mutate()` input `..1`.
#> x Must subset columns with a valid subscript vector.
#> x Can't convert from <double> to <integer> due to loss of precision.
#> ℹ Input `..1` is `foo(c(Sepal.Length))`.

reprex package (v0.3.0)

于 2021-04-14 创建

目前我正在使用一种我不太喜欢的解决方法。我在 foo 中使用省略号 ... 以便我可以使用不需要记录的附加参数来调用它。现在 foo 可以用 flag 参数调用,在这种情况下 foo 知道不必引用这些列。

但是,我认为这不是一个干净的解决方案。我更喜欢某种函数,如果尚未引用则引用,或者在将列名称传递给 bar.

时恢复列名称环境的函数

另一种可能的解决方案是首先评估 bar 中的列,然后将列名称作为字符串粘贴到 foo。我没有尝试过,它应该可以工作,因为 tidyselect 接受字符串,但是我想避免评估 bar 中的列名,因为它看起来不太高效。

欢迎任何其他想法。

# workaround using `...`
foo <- function(cols, ...){
  
  dots <- rlang::list2(...)
  if (is.null(dots$flag)) {
    cols <- enquo(cols)
  }
  
  data <- cur_data()
  vars <- tidyselect::eval_select(cols, data)
  out <- data[, vars]
  
  names(out) <- paste0("new_", names(out))
  out
}

bar <- function(df, .cols) {
  .cols <- rlang::enquo(.cols)
  mutate(df, foo(.cols, flag = TRUE))
}


# working
myiris %>%
  mutate(foo(c(Sepal.Length)))

#> # A tibble: 150 x 6
#>    Sepal.Length Sepal.Width Petal.Length Petal.Width Species new_Sepal.Length
#>           <dbl>       <dbl>        <dbl>       <dbl> <fct>              <dbl>
#>  1          5.1         3.5          1.4         0.2 setosa               5.1
#>  2          4.9         3            1.4         0.2 setosa               4.9
#>  3          4.7         3.2          1.3         0.2 setosa               4.7
#>  4          4.6         3.1          1.5         0.2 setosa               4.6
#>  5          5           3.6          1.4         0.2 setosa               5  
#>  6          5.4         3.9          1.7         0.4 setosa               5.4
#>  7          4.6         3.4          1.4         0.3 setosa               4.6
#>  8          5           3.4          1.5         0.2 setosa               5  
#>  9          4.4         2.9          1.4         0.2 setosa               4.4
#> 10          4.9         3.1          1.5         0.1 setosa               4.9
#> # … with 140 more rows

# working
myiris %>%
  bar(Sepal.Length)

#> # A tibble: 150 x 6
#>    Sepal.Length Sepal.Width Petal.Length Petal.Width Species new_Sepal.Length
#>           <dbl>       <dbl>        <dbl>       <dbl> <fct>              <dbl>
#>  1          5.1         3.5          1.4         0.2 setosa               5.1
#>  2          4.9         3            1.4         0.2 setosa               4.9
#>  3          4.7         3.2          1.3         0.2 setosa               4.7
#>  4          4.6         3.1          1.5         0.2 setosa               4.6
#>  5          5           3.6          1.4         0.2 setosa               5  
#>  6          5.4         3.9          1.7         0.4 setosa               5.4
#>  7          4.6         3.4          1.4         0.3 setosa               4.6
#>  8          5           3.4          1.5         0.2 setosa               5  
#>  9          4.4         2.9          1.4         0.2 setosa               4.4
#> 10          4.9         3.1          1.5         0.1 setosa               4.9
#> # … with 140 more rows

reprex package (v0.3.0)

于 2021-04-14 创建

也许我不明白用例,但是当您将它们从 bar() 传递到 foo() 时,为什么必须引用这些列?如果您取消引用输入,一切都会按预期进行:

bar <- function(df, .cols) {
  .cols <- rlang::enquo(.cols)
  mutate(df, foo(!!.cols))      # <--- unquote before passing to foo()
}

# Or alternatively
bar <- function(df, .cols) {mutate(df, foo( {{.cols}} ))}

myiris %>%
  bar(Sepal.Length)             # works