Inspect the dots

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.

# Misspelled
weighted.mean(c(1, 0, -1), wt = c(10, 0, 0))
#> [1] 0
mean(c(1:9, 100), trim = 0.1)
#> [1] 5.5

# Correct
weighted.mean(c(1, 0, -1), w = c(10, 0, 0))
#> [1] 1
mean(c(1:9, 100), trim = 0.1)
#> [1] 5.5

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.

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 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:

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, 
        ...
      )
    )
}
x <- c("x1", "x100", "x2")
str_sort(x)
#> [1] "x1"   "x100" "x2"
str_sort(x, numeric = TRUE)
#> [1] "x1"   "x2"   "x100"

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”, Chapter 5.)

TODO: Update this! It’s now wrong!

However, stri_opts_collator() deliberately ignores any arguments in .... This means that misspellings are silently ignored:

str_sort(x, numric = TRUE)

We can work around this behaviour by adding check_dots_used() to str_sort():

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)
#> Error in stringi::stri_opts_collator(locale, numeric = numeric, ...): unused argument (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().