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.
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 afile
, or inline astext
. If you supply both,path
wins.In
var()
you can remove missing values by either settingna.rm = TRUE
oruse = "complete.obs"
. If you supply both,use
wins.rvest::html_element()
allows you to select HTML elements either with acss
selector or anxpath
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 withbreaks
andlabels
(like all other scale functions) or withdate_breaks
anddate_labels
. If you set both values in a pair, thedate_
version wins.forcats::fct_other()
allows you to eitherkeep
ordrop
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 separatetext
andfile
arguments. Instead, you can supply raw text directly tofile
by wrapping it inI()
.rvest could have done this by providing
css()
andxpath()
helper functions so instead of writinghtml |> html_element(css = "p a")
you’d writehtml |> 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()
andfct_other_keep()
. This isn’t much less typing for the user since the options are eitherfct_other(x, drop = "abc")
orfct_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
orxpath
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.
(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.