12 Enumerate possible options

12.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 user what the possible values, and makes it easy to generate an informative error message for inappropriate inputs.

12.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”.

  • In RSiteSearch(), you can restrict results to be “functions”, “vignettes”, “views”, or any combination of the three.

12.3 Why is it important?

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 tooltips and autocomplete, without having to look at the documentation.

12.4 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() which checks that the value comes from the known good set. This interface pattern is often coupled with an implementation that uses switch().

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 3 2 4 5 6

I generally believe that partial matching is a bad idea, because it makes code harder to read. rlang::arg_match() is an alternative to match.args() that doesn’t support partial matching. Instead it 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: `ties.method` must be one of "average", "first", "last", "random", "max", or "min".
#> Did you mean "random"?

12.4.1 How keep defaults short?

This technique is a best used when the set of possible values is short. You can see that it’s already getting unwieldy in rank(). If you have a long list of possibilities, there are two options that you could use from Chapter 14. Unfortunately both approaches have major downsides:

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

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

    The downside of this approach is that you can no longer see which values are permitted, and you’d have to describe them separately in the documentation. You can, however, still see the default value in the function speci.

  • Store the options in an exported function, and use it in the defaults:

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

    The downside of this approach is that when looking at the function spec, you can no longer easily see the default value, or the set of possible values. However, the possible values can easily be found programmatically.

    This is more worthwhile if you want to share the permitted values across multiple functions. 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.