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

问题描述

我的问题如下。我有一个foo 内工作的函数 dplyr::mutate。此函数接受 tidyselect 语法。我想构建一个包装函数 bar,它也应该支持 tidyselect 语法。我正在寻找一种干净的方式将 tidyselect 列从 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 年 4 月 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,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 年 4 月 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,names(out))
  out
}

bar <- function(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 年 4 月 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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...