-
Notifications
You must be signed in to change notification settings - Fork 53
/
dots-inspect.qmd
131 lines (98 loc) · 5 KB
/
dots-inspect.qmd
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
# Inspect the dots {#sec-dots-inspect}
```{r}
#| include = FALSE
source("common.R")
```
<!-- call dots-details-s3 ? -->
## What's the pattern?
Whenever you use `...` in an S3 generic to allow methods to add custom arguments, you should inspect the dots to make sure that every argument is used.
You can also use this same approach when passing `...` to an overly permissive function.
## What are some examples?
If you don't use this technique it is easy to end up with functions that silently return the incorrect result when argument names are misspelled.
```{r}
# Misspelled
weighted.mean(c(1, 0, -1), wt = c(10, 0, 0))
mean(c(1:9, 100), trim = 0.1)
# Correct
weighted.mean(c(1, 0, -1), w = c(10, 0, 0))
mean(c(1:9, 100), trim = 0.1)
```
## How do I do it?
Add a call to `rlang::check_dots_used()` in the generic before the call to `UseMethod()`.
This automatically adds an on exit handler, which checks that ever element of `...` has been evaluated just prior to the function returnning.
You can see this in action by creating a safe wrapper around `cut()`, which has different arguments for its numeric and date methods.
```{r}
safe_cut <- function(x, breaks, ..., right = TRUE) {
rlang::check_dots_used()
UseMethod("safe_cut")
}
safe_cut.numeric <- function(x, breaks, ..., right = TRUE, include.lowest = FALSE) {
cut(x, breaks = breaks, right = right, include.lowest = include.lowest)
}
safe_cut.Date <- function(x, breaks, ..., right = TRUE, start.on.monday = TRUE) {
cut(x, breaks = breaks, right = right, start.on.monday = start.on.monday)
}
```
### What are the limitations?
Accurately detecting this problem is hard because no one place has all the information needed to tell if an argument is superfluous or not (the precise details are beyond the scope of this text).
Instead rlang takes advantage of R's [lazy evaluation](https://adv-r.hadley.nz/functions.html#lazy-evaluation) and inspects the internal components of `...` to see if their evaluation has been forced.
If a function is called primarily for its side-effects, the error will occur after the side-effect has happened, making for a confusing result.
Here the best we can do is a warning, generated by `rlang::check_dots_used(error = function(e) warn(e))`
If a function captures the components of `...` using `enquo()` or `match.call()`, you can not use this technique.
This also means that if you use `check_dots_used()`, the method author can not choose to add a quoted argument.
I think this is ok because quoting vs. evaluating is part of the interface of the generic, so methods should not change this interface, and it's fine for the author of the generic to make that decision for all method authors.
### What are other uses?
This same technique can also be used when you are wrapping other functions.
For example, `stringr::str_sort()` takes `...` and passes it on to `stringi::stri_opts_collator()`.
As of March 2019, `str_sort()` looked like this:
```{r}
str_sort <- function(x, decreasing = FALSE, na_last = TRUE, locale = "en", numeric = FALSE, ...)
{
stringi::stri_sort(x,
decreasing = decreasing,
na_last = na_last,
opts_collator = stringi::stri_opts_collator(
locale,
numeric = numeric,
...
)
)
}
```
```{r}
x <- c("x1", "x100", "x2")
str_sort(x)
str_sort(x, numeric = TRUE)
```
This is wrapper is useful because it decouples `str_sort()` from the `stri_opts_collator()` meaning that if `stri_opts_collator()` gains new arguments users of `str_sort()` can take advantage of them immediately.
But most of the arguments in `stri_opts_collator()` are sufficiently arcane that they don't need to be exposed directly in stringr, which is designed to minimise the cognitive load of the user, by hiding some of the full complexity of string handling.
(The importance of the `locale` argument comes up in "hidden inputs", @sec-inputs-explicit.)
TODO: Update this!
It's now wrong!
However, `stri_opts_collator()` deliberately ignores any arguments in `...`.
This means that misspellings are silently ignored:
```{r}
#| eval: false
str_sort(x, numric = TRUE)
```
We can work around this behaviour by adding `check_dots_used()` to `str_sort()`:
```{r}
#| error = TRUE
str_sort <- function(x, decreasing = FALSE, na_last = TRUE, locale = "en", numeric = FALSE, ...)
{
rlang::check_dots_used()
stringi::stri_sort(x,
decreasing = decreasing,
na_last = na_last,
opts_collator = stringi::stri_opts_collator(
locale,
numeric = numeric,
...
)
)
}
str_sort(x, numric = TRUE)
```
Note, however, that it's better to figure out why `stri_opts_collator()` ignores `...` in the first place.
You can see that discussion at <https://github.com/gagolews/stringi/issues/347>.
See <https://github.com/r-lib/devtools/issues/2016> for discussion about using this in another discussion about using this in `devtools::install_github()` which is an similar situation, but with a more complicated chain of calls: `devtools::install_github()` -\> `install.packages()` -\> `download.file()`.