10  Enumerate possible options

10.1 What’s the pattern?

If the possible values of an argument are a small set of strings, set the default argument to the set of possible values, and then use match.arg() or rlang::arg_match() in the function body. This convention advertises to the knowledgeable user1 what the possible values, and makes it easy to generate an informative error message for inappropriate inputs. This interface is often coupled with an implementation that uses switch().

This convention makes it possible to advertise the possible set of values for an argument. The advertisement happens in the function specification, so you see in tool tips and autocomplete, without having to look at the documentation.

10.2 What are some examples?

  • In difftime(), units can be any one of “auto”, “secs”, “mins”, “hours”, “days”, or “weeks”.

  • In format(), justify can be “left”, “right”, “center”, or “none”.

  • In trimws(), you can choose which side to remove whitespace from: “both”, “left”, or “right”.

  • In rank(), you can select the ties.method from one of “average”, “first”, “last”, “random”, “max”, or “min”.

10.3 How do I use it?

To use this technique, set the default value to a character vector, where the first value is the default. Inside the function, use match.arg() or rlang::arg_match() to check that the value comes from the known good set, and pick the default if none is suppled.

Take rank(), for example. The heart of its implementation looks like this:

rank <- function(x, 
                 ties.method = 
                  c("average", "first", "last", "random", "max", "min")
                 ) {
  
  ties.method <- match.arg(ties.method)
  
  switch(ties.method, 
    average = , 
    min = , 
    max = .Internal(rank(x, length(x), ties.method)), 
    first = sort.list(sort.list(x)),
    last = sort.list(rev.default(sort.list(x, decreasing = TRUE))), 
    random = sort.list(order(x, stats::runif(length(x))))
  )
}

x <- c(1, 2, 2, 3, 3, 3)

rank(x)
#> [1] 1.0 2.5 2.5 5.0 5.0 5.0
rank(x, ties.method = "first")
#> [1] 1 2 3 4 5 6
rank(x, ties.method = "min")
#> [1] 1 2 2 4 4 4

Note that match.arg() will automatically throw an error if the value is not in the set:

rank(x, ties.method = "middle")
#> Error in match.arg(ties.method): 'arg' should be one of "average", "first", "last", "random", "max", "min"

It also supports partial matching so that the following code is shorthand for ties.method = "random":

rank(x, ties.method = "r")
#> [1] 1 2 3 6 5 4

We prefer to avoid partial matching because while it saves a little time writing the code, it makes reading the code less clear. rlang::arg_match() is an alternative to match.arg() that doesn’t support partial matching. It instead provides a helpful error message:

rank2 <- function(x, ties.method = c("average", "first", "last", "random", "max", "min")) {
  ties.method <- rlang::arg_match(ties.method)
  rank(x, ties.method = ties.method)
}

rank2(x, ties.method = "r")
#> Error in `rank2()`:
#> ! `ties.method` must be one of "average", "first", "last", "random",
#>   "max", or "min", not "r".
#> ℹ Did you mean "random"?

# It also provides a suggestion if you misspell the argument
rank2(x, ties.method = "avarage")
#> Error in `rank2()`:
#> ! `ties.method` must be one of "average", "first", "last", "random",
#>   "max", or "min", not "avarage".
#> ℹ Did you mean "average"?

10.3.1 How keep defaults short?

This technique is a best used when the set of possible values is short as otherwise you run the risk of dominating the function spec with this one argument (Chapter 9). If you have a long list of possibilities, there are three possible solutions:

  • Set a single default and supply the possible values to match.arg()/arg_match():

    rank2 <- function(x, ties.method = "average") {
      ties.method <- arg_match(
        ties.method, 
        c("average", "first", "last", "random", "max", "min")
      )
    }
  • If the values are used by many functions, you can store the options in an exported vector:

    ties.methods <- c("average", "first", "last", "random", "max", "min")
    
    rank2 <- function(x, ties.method = ties.methods) {
      ties.method <- arg_match(ties.method)
    }

    For example stats::p.adjust(), stats::pairwise.prop.test(), stats::pairwise.t.test(), stats::pairwise.wilcox.test() all use p.adjust.method = p.adjust.methods.

  • You can store the options in a exported named list2. That has the advantage that you can advertise both the source of the values, and the defaults, and the user gets a nice auto-complete of the possible values.

    library(rlang)
    ties <- as.list(set_names(c("average", "first", "last", "random", "max", "min")))
    
    rank2 <- function(x, ties.method = ties$average) {
      ties.method <- arg_match(ties.method, names(ties))
    }

  1. The main downside of this technique is that many users aren’t aware of this convention and that the first value of the vector will be used as a default.↩︎

  2. Thanks to Brandon Loudermilk↩︎