15  Mutually exclusive arguments

15.1 What’s the pattern?

If a function needs to have mutually exclusive arguments (i.e. you must supply only one of theme) make sure you check that only one is supplied in order to give a clear error message. Avoid implementing some precedence order where if both a and b are supplied, b silently wins.

The main drawback of a pair of mutually exclusive arguments is that there’s no way to the exclusivity clear from the function signature alone. Instead, the best you can do is make both appear required (by leaving them free from defaults) and then performing your own check in the body of the function.

This technique should generally be reserved for a pair of possible arguments. If there are more than two, that is generally a sign you reach for another technique. You also need to be confident that there will only ever be two options, as switching to a different approach later will be painful.

(In the case of required args, you might want to consider putting them after . This violations Chapter 8, but forces the user to name the arguments which will make the code easier to read)

15.2 What are some examples?

  • read.table() allows you to supply data either with a path to a file, or inline as text. If you supply both, path wins.

  • In var() you can remove missing values by either setting na.rm = TRUE or use = "complete.obs". If you supply both, use wins.

  • rvest::html_element() allows you to select HTML elements either with a css selector or an xpath expression. If supply neither, or both, you get an error.

  • In ggplot2::scale_x_date() and friends you can specify the breaks and labels either with breaks and labels (like all other scale functions) or with date_breaks and date_labels. If you set both values in a pair, the date_ version wins.

  • forcats::fct_other() allows you to either keep or drop specified factor values. If supply neither, or both, you get an error.

  • dplyr::relocate() has optional .before and .after arguments.

15.3 Alternatives

  • Use a strategy pattern. For example, readr::read_csv() doesn’t have separate text and file arguments. Instead, you can supply raw text directly to file by wrapping it in I().

    rvest could have done this by providing css() and xpath() helper functions so instead of writing html |> html_element(css = "p a") you’d write html |> html_element(css("p a")). I think probably would have netted about about the same, and is a reasonable alternative. The big advantage of this pattern would be that it’s easier to extend if we discovered other ways of describing the elements to select.

  • Use separate functions. For example could have fct_other_drop() and fct_other_keep(). This isn’t much less typing for the user since the options are either fct_other(x, drop = "abc") or fct_other_drop(x, "abc").

    This only works if the pair of arguments is relatively rare in your overall API. It wouldn’t work, for example, in rvest, since a large number of functions take either css or xpath so it would almost double the number of functions in the package.

  • cutree() is an example where I think mutually exclusive arguments shine: it’s so simple

15.4 How do you use this pattern?

If the arguments are required, leave the mutually exclusive arguments free from defaults then use rlang::check_exclusive(). If they’re optional, give them NULL arguments.

fct_drop <- function(f, drop, keep) {
  rlang::check_exclusive(drop, keep)
}

fct_drop(factor())
#> Error in `fct_drop()`:
#> ! One of `drop` or `keep` must be supplied.

fct_drop(factor(), keep = "a", drop = "b")
#> Error in `fct_drop()`:
#> ! Exactly one of `drop` or `keep` must be supplied.

(If the arguments are optional, you’ll need .require = FALSE until https://github.com/r-lib/rlang/issues/1647)

If you don’t want to take a dependency on rlang, you can perform the check “by hand” with xor() and missing():

fct_drop <- function(f, drop, keep) {
  if (!xor(missing(keep), missing(drop))) {
    stop("Exactly one of `keep` and `drop` must be supplied")
  }  
}
fct_drop(factor())
#> Error in fct_drop(factor()): Exactly one of `keep` and `drop` must be supplied

fct_drop(factor(), keep = "a", drop = "b")
#> Error in fct_drop(factor(), keep = "a", drop = "b"): Exactly one of `keep` and `drop` must be supplied

In the documentation, document the pair of arguments together, and make it clear that only one of the pair can be supplied:

#' @param keep,drop Pick one of `keep` and `drop`:
#'   * `keep` will preserve listed levels, replacing all others with 
#'     `other_level`.
#'   * `drop` will replace listed levels with `other_level`, keeping all
#'     as is.