```{r, child = "setup.Rmd", include = FALSE} ``` In this guide we compare ways of taking multiple columns in a single function argument. As a refresher (see the [programming patterns][topic-data-mask-programming] article), there are two common ways of passing arguments to [data-masking][topic-data-mask] functions. For single arguments, embrace with `r link("{{")`: ```{r, comment = "#>", collapse = TRUE} my_group_by <- function(data, var) { data %>% dplyr::group_by({{ var }}) } my_pivot_longer <- function(data, var) { data %>% tidyr::pivot_longer({{ var }}) } ``` For multiple arguments in `...`, pass them on to functions that also take `...` like `group_by()`, or pass them within `c()` for functions taking tidy selection in a single argument like `pivot_longer()`: ```{r, comment = "#>", collapse = TRUE} # Pass dots through my_group_by <- function(.data, ...) { .data %>% dplyr::group_by(...) } my_pivot_longer <- function(.data, ...) { .data %>% tidyr::pivot_longer(c(...)) } ``` But what if you want to take multiple columns in a single named argument rather than in `...`? # Using tidy selections The idiomatic tidyverse way of taking multiple columns in a single argument is to take a _tidy selection_ (see the [Argument behaviours][topic-data-mask-programming] section). In tidy selections, the syntax for passing multiple columns in a single argument is `c()`: ```r mtcars %>% tidyr::pivot_longer(c(am, cyl, vs)) ``` Since `{{` inherits behaviour, this implementation of `my_pivot_longer()` automatically allows multiple columns passing: ```r my_pivot_longer <- function(data, var) { data %>% tidyr::pivot_longer({{ var }}) } mtcars %>% my_pivot_longer(c(am, cyl, vs)) ``` For `group_by()`, which takes data-masked arguments, we'll use `across()` as a _bridge_ (see [Bridge patterns][topic-data-mask-programming]). ```r my_group_by <- function(data, var) { data %>% dplyr::group_by(across({{ var }})) } mtcars %>% my_group_by(c(am, cyl, vs)) ``` When embracing in tidyselect context or using `across()` is not possible, you might have to implement tidyselect behaviour manually with `tidyselect::eval_select()`. # Using external defusal To implement an argument with tidyselect behaviour, it is necessary to [defuse][topic-defuse] the argument. However defusing an argument which had historically behaved like a regular argument is a rather disruptive breaking change. This is why we could not implement tidy selections in ggplot2 facetting functions like `facet_grid()` and `facet_wrap()`. An alternative is to use external defusal of arguments. This is what formula interfaces do for instance. A modelling function takes a formula in a regular argument and the formula defuses the user code: ```r my_lm <- function(data, f, ...) { lm(f, data, ...) } mtcars %>% my_lm(disp ~ drat) ``` Once created, the defused expressions contained in the formula are passed around like a normal argument. A similar approach was taken to update `facet_` functions to tidy eval. The `vars()` function (a simple alias to [quos()]) is provided so that users can defuse their arguments externally. ```r ggplot2::facet_grid( ggplot2::vars(cyl), ggplot2::vars(am, vs) ) ``` You can implement this approach by simply taking a list of defused expressions as argument. This list can be passed the usual way to other functions taking such lists: ```r my_facet_grid <- function(rows, cols, ...) { ggplot2::facet_grid(rows, cols, ...) } ``` Or it can be spliced with [`!!!`]: ```r my_group_by <- function(data, vars) { stopifnot(is_quosures(vars)) data %>% dplyr::group_by(!!!vars) } mtcars %>% my_group_by(dplyr::vars(cyl, am)) ``` # A non-approach: Parsing lists Intuitively, many programmers who want to take a list of expressions in a single argument try to defuse an argument and parse it. The user is expected to supply multiple arguments within a `list()` expression. When such a call is detected, the arguments are retrieved and spliced with `!!!`. Otherwise, the user is assumed to have supplied a single argument which is injected with `!!`. An implementation along these lines might look like this: ```{r, comment = "#>", collapse = TRUE} my_group_by <- function(data, vars) { vars <- enquo(vars) if (quo_is_call(vars, "list")) { expr <- quo_get_expr(vars) env <- quo_get_env(vars) args <- as_quosures(call_args(expr), env = env) data %>% dplyr::group_by(!!!args) } else { data %>% dplyr::group_by(!!vars) } } ``` This does work in simple cases: ```{r, comment = "#>", collapse = TRUE} mtcars %>% my_group_by(cyl) %>% dplyr::group_vars() mtcars %>% my_group_by(list(cyl, am)) %>% dplyr::group_vars() ``` However this parsing approach quickly shows limits: ```r mtcars %>% my_group_by(list2(cyl, am)) #> Error in `group_by()`: Can't add columns. #> i `..1 = list2(cyl, am)`. #> i `..1` must be size 32 or 1, not 2. ``` Also, it would be better for overall consistency of interfaces to use the tidyselect syntax `c()` for passing multiple columns. In general, we recommend to use either the tidyselect or the external defusal approaches.