% Generated by roxygen2: do not edit by hand % Please edit documentation in R/topic-nse.R \name{topic-multiple-columns} \alias{topic-multiple-columns} \title{Taking multiple columns without \code{...}} \description{ In this guide we compare ways of taking multiple columns in a single function argument. As a refresher (see the \link[=topic-data-mask-programming]{programming patterns} article), there are two common ways of passing arguments to \link[=topic-data-mask]{data-masking} functions. For single arguments, embrace with \ifelse{html}{\code{\link[=embrace-operator]{\{\{}}}{\verb{\{\{}}: \if{html}{\out{
}}\preformatted{my_group_by <- function(data, var) \{ data \%>\% dplyr::group_by(\{\{ var \}\}) \} my_pivot_longer <- function(data, var) \{ data \%>\% tidyr::pivot_longer(\{\{ var \}\}) \} }\if{html}{\out{
}} For multiple arguments in \code{...}, pass them on to functions that also take \code{...} like \code{group_by()}, or pass them within \code{c()} for functions taking tidy selection in a single argument like \code{pivot_longer()}: \if{html}{\out{
}}\preformatted{# Pass dots through my_group_by <- function(.data, ...) \{ .data \%>\% dplyr::group_by(...) \} my_pivot_longer <- function(.data, ...) \{ .data \%>\% tidyr::pivot_longer(c(...)) \} }\if{html}{\out{
}} But what if you want to take multiple columns in a single named argument rather than in \code{...}? } \section{Using tidy selections}{ The idiomatic tidyverse way of taking multiple columns in a single argument is to take a \emph{tidy selection} (see the \link[=topic-data-mask-programming]{Argument behaviours} section). In tidy selections, the syntax for passing multiple columns in a single argument is \code{c()}: \if{html}{\out{
}}\preformatted{mtcars \%>\% tidyr::pivot_longer(c(am, cyl, vs)) }\if{html}{\out{
}} Since \verb{\{\{} inherits behaviour, this implementation of \code{my_pivot_longer()} automatically allows multiple columns passing: \if{html}{\out{
}}\preformatted{my_pivot_longer <- function(data, var) \{ data \%>\% tidyr::pivot_longer(\{\{ var \}\}) \} mtcars \%>\% my_pivot_longer(c(am, cyl, vs)) }\if{html}{\out{
}} For \code{group_by()}, which takes data-masked arguments, we'll use \code{across()} as a \emph{bridge} (see \link[=topic-data-mask-programming]{Bridge patterns}). \if{html}{\out{
}}\preformatted{my_group_by <- function(data, var) \{ data \%>\% dplyr::group_by(across(\{\{ var \}\})) \} mtcars \%>\% my_group_by(c(am, cyl, vs)) }\if{html}{\out{
}} When embracing in tidyselect context or using \code{across()} is not possible, you might have to implement tidyselect behaviour manually with \code{tidyselect::eval_select()}. } \section{Using external defusal}{ To implement an argument with tidyselect behaviour, it is necessary to \link[=topic-defuse]{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 \code{facet_grid()} and \code{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: \if{html}{\out{
}}\preformatted{my_lm <- function(data, f, ...) \{ lm(f, data, ...) \} mtcars \%>\% my_lm(disp ~ drat) }\if{html}{\out{
}} Once created, the defused expressions contained in the formula are passed around like a normal argument. A similar approach was taken to update \code{facet_} functions to tidy eval. The \code{vars()} function (a simple alias to \code{\link[=quos]{quos()}}) is provided so that users can defuse their arguments externally. \if{html}{\out{
}}\preformatted{ggplot2::facet_grid( ggplot2::vars(cyl), ggplot2::vars(am, vs) ) }\if{html}{\out{
}} 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: \if{html}{\out{
}}\preformatted{my_facet_grid <- function(rows, cols, ...) \{ ggplot2::facet_grid(rows, cols, ...) \} }\if{html}{\out{
}} Or it can be spliced with \code{\link{!!!}}: \if{html}{\out{
}}\preformatted{my_group_by <- function(data, vars) \{ stopifnot(is_quosures(vars)) data \%>\% dplyr::group_by(!!!vars) \} mtcars \%>\% my_group_by(dplyr::vars(cyl, am)) }\if{html}{\out{
}} } \section{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 \code{list()} expression. When such a call is detected, the arguments are retrieved and spliced with \verb{!!!}. Otherwise, the user is assumed to have supplied a single argument which is injected with \verb{!!}. An implementation along these lines might look like this: \if{html}{\out{
}}\preformatted{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) \} \} }\if{html}{\out{
}} This does work in simple cases: \if{html}{\out{
}}\preformatted{mtcars \%>\% my_group_by(cyl) \%>\% dplyr::group_vars() #> [1] "cyl" mtcars \%>\% my_group_by(list(cyl, am)) \%>\% dplyr::group_vars() #> [1] "cyl" "am" }\if{html}{\out{
}} However this parsing approach quickly shows limits: \if{html}{\out{
}}\preformatted{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. }\if{html}{\out{
}} Also, it would be better for overall consistency of interfaces to use the tidyselect syntax \code{c()} for passing multiple columns. In general, we recommend to use either the tidyselect or the external defusal approaches. } \keyword{internal}