forked from tidyverse/tidyeval
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdotdotdot.Rmd
More file actions
208 lines (139 loc) · 7.67 KB
/
Copy pathdotdotdot.Rmd
File metadata and controls
208 lines (139 loc) · 7.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
```{r setup, include = FALSE}
source("setup.R")
library("dplyr")
```
# Dealing with multiple arguments {#multiple}
In the first chapter we have created `grouped_mean()`, a function that takes one grouping variable and one summary variable and computes the grouped average. It would make sense to take multiple grouping variables instead of just one. Quoting and unquoting multiple variables is pretty much the same process as for single arguments:
* Unquoting multiple arguments requires a variant of `!!`, the big bang operator `!!!`.
* Quoting multiple arguments can be done in two ways: internal quoting with the plural variant `enquos()` and external quoting with `vars()`.
## The `...` argument
The dot-dot-dot argument is one of the nicest aspects of the R language. A function that takes `...` accepts any number of arguments, named or unnamed. As a programmer you can do three things with `...`:
1. **Evaluate** the arguments contained in the dots and materialise them in a list by forwarding the dots to `list()`:
```{r}
materialise <- function(data, ...) {
dots <- list(...)
dots
}
```
The dots names conveniently become the names of the list:
```{r}
materialise(mtcars, 1 + 2, important_name = letters)
```
1. **Quote** the arguments in the dots with `enquos()`:
```{r}
capture <- function(data, ...) {
dots <- enquos(...)
dots
}
```
All arguments passed to `...` are automatically quoted and returned as a list. The names of the arguments become the names of that list:
```{r}
capture(mtcars, 1 + 2, important_name = letters)
```
1. **Forward** the dots to another function:
```{r}
forward <- function(data, ...) {
forwardee(...)
}
```
When dots are forwarded the names of arguments in `...` are matched to the arguments of the forwardee:
```{r}
forwardee <- function(foo, bar, ...) {
list(foo = foo, bar = bar, ...)
}
```
Let's call the forwarding function with a bunch of named and unnamed arguments:
```{r}
forward(mtcars, bar = 100, 1, 2, 3)
```
The unnamed argument `1` was matched to `foo` positionally. The named argument `bar` was matched to `bar`. The remaining arguments were passed in order.
For the purpose of writing tidy eval functions the last two techniques are important. There are two distinct situations:
1. You don't need to modify the arguments in any way, just passing them through. Then simply forward `...` to other quoting functions in the ordinary way.
1. You'd like to change the argument names (which become column names in `dplyr::mutate()` calls) or modify the arguments themselves (for instance negate a `dplyr::select()`ion). In that case you'll need to use `enquos()` to *quote* the arguments in the dots. You'll then pass the quoted arguments to other quoting functions by *forwarding* them with the help of `!!!`.
## Simple forwarding of `...`
If you are not modifying the arguments in `...` in any way and just want to pass them to another quoting function, just forward `...` like usual! There is no need for quoting and unquoting because of the magic of forwarding. The arguments in `...` are transported to their final destination where they will be quoted.
The function `grouped_mean()` is still going to need some remodelling because it is good practice to take all important named arguments before the dots. Let's start by swapping `grouped_var` and `summary_var`:
```{r}
grouped_mean <- function(data, summary_var, group_var) {
summary_var <- enquo(summary_var)
group_var <- enquo(group_var)
data %>%
group_by(!!group_var) %>%
summarise(mean = mean(!!summary_var))
}
```
Then we replace `group_var` with `...` and pass it to `group_by()`:
```{r}
grouped_mean <- function(data, summary_var, ...) {
summary_var <- enquo(summary_var)
data %>%
group_by(...) %>%
summarise(mean = mean(!!summary_var))
}
```
It is good practice to make one final adjustment. Because arguments in `...` can have arbitrary names, we don't want to "use up" valid names. In tidyverse packages we use the convention of prefixing named arguments with a dot so that conflicts are less likely:
```{r}
grouped_mean <- function(.data, .summary_var, ...) {
.summary_var <- enquo(.summary_var)
.data %>%
group_by(...) %>%
summarise(mean = mean(!!.summary_var))
}
```
Let's check this function now works with any number of grouping variables:
```{r}
grouped_mean(mtcars, disp, cyl, am)
grouped_mean(mtcars, disp, cyl, am, vs)
```
## Quote multiple arguments
When we need to modify the arguments or their names, we can't simply forward the dots. We'll have to quote and unquote with the plural variants of `enquo()` and `!!`.
- We'll quote the dots with `enquos()`.
- We'll unquote-splice the quoted dots with `!!!`.
While the singular `enquo()` returns a single quoted argument, the plural variant `enquos()` returns a list of quoted arguments. Let's use it to quote the dots:
```{r}
grouped_mean2 <- function(.data, .summary_var, ...) {
.summary_var <- enquo(.summary_var)
.group_vars <- enquos(...)
data %>%
group_by(!!.group_vars) %>%
summarise(mean = mean(!!.summary_var))
}
```
`grouped_mean()` now accepts and automatically quotes any number of grouping variables. However it doesn't work quite yet:
**FIXME**: Depend on dev rlang to get a better error message.
```{r, error = TRUE }
grouped_mean2(mtcars, disp, cyl, am)
```
Instead of *forwarding* the individual arguments to `group_by()` we have passed the list of arguments itself! Unquoting is not the right operation here. Fortunately tidy eval provides a special operator that makes it easy to forward a list of arguments.
## Unquote multiple arguments
The **unquote-splice** operator `!!!` takes each element of a list and unquotes them as independent arguments to the surrounding function call. The arguments are *spliced* in the function call. This is just what we need for forwarding multiple quoted arguments.
Let's use `qq_show()` to observe the difference between `!!` and `!!!` in a `group_by()` expression. We can only use `enquos()` within a function so let's create a list of quoted names for the purpose of experimenting:
```{r}
vars <- list(
quote(cyl),
quote(am)
)
```
`qq_show()` shows the difference between unquoting a list and unquote-splicing a list:
```{r}
rlang::qq_show(group_by(!!vars))
rlang::qq_show(group_by(!!!vars))
```
When we use the unquote operator `!!`, `group_by()` gets a list of expressions. When we unquote-splice with `!!!`, the expressions are forwarded as individual arguments to `group_by()`. Let's use the latter to fix `grouped_mean2()`:
```{r}
grouped_mean2 <- function(.data, .summary_var, ...) {
.summary_var <- enquo(.summary_var)
.group_vars <- enquos(...)
.data %>%
group_by(!!!.group_vars) %>%
summarise(mean = mean(!!.summary_var))
}
```
The quote and unquote version of `grouped_mean()` does a bit more work but is functionally identical to the forwarding version:
```{r}
grouped_mean(mtcars, disp, cyl, am)
grouped_mean2(mtcars, disp, cyl, am)
```
When does it become useful to do all this extra work? Whenever you need to modify the arguments or their names.
Up to now we have used the quote-and-unquote pattern to pass quoted arguments to other quoting functions "as is". With this simple and powerful pattern you can extract complex combinations of quoting verbs into reusable functions.
However tidy eval provides much more flexibility. It is a general purpose meta-programming framework that makes it easy to modify quoted arguments before evaluation. In the next section you'll learn about basic metaprogramming patterns that will allow you to modify expressions before passing them on to other functions.