% 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}