The idea
assert gives you small, explicit helpers for checking
the inputs and outputs of your functions. You place assertions at the
top of a function; if one fails, it throws an error and the function
stops before any real work happens.
The examples below import the functions they use with
box::use(assert[...]), so it is always clear where each
function comes from. If you prefer, library(assert) works
too.
Every assertion shares the same contract:
- it checks one condition,
- it returns its input invisibly on success, so checks stack neatly,
- it throws a clear error pointing at your function on failure.
Guarding inputs
box::use(
assert[
assert_numeric,
assert_no_missing_values,
assert_scalar_numeric
]
)
scale_values <- function(values, factor) {
assert_numeric(values)
assert_no_missing_values(values)
assert_scalar_numeric(factor)
return(values * factor)
}
scale_values(c(1, 2, 3), 10)
#> [1] 10 20 30Read top to bottom, the assertions describe the preconditions:
“values is numeric, has no missing values;
factor is a single number.” When something is wrong, the
message names the argument and the calling function:
scale_values(c(1, NA, 3), 10)
#> Error in `scale_values()`:
#> ! `values` must not contain missing values.Scalars reject NA
A assert_scalar_* check requires a single,
non-NA value, because a lone value that is secretly
NA is almost always a mistake. The plain vector checks
(assert_numeric(), assert_character(), …)
still allow NA, since a missing value among many is often
legitimate.
box::use(assert[assert_scalar_numeric])
assert_scalar_numeric(NA)
#> Error:
#> ! `NA` must be a single numeric value.Optional arguments with null_ok
Arguments that default to NULL are optional. By default
an assertion rejects NULL; pass null_ok = TRUE
to allow it while still validating any value that is
supplied.
box::use(assert[assert_scalar_character])
greet <- function(name, title = NULL) {
assert_scalar_character(name)
assert_scalar_character(title, null_ok = TRUE)
return(if (is.null(title)) name else paste(title, name))
}
greet("Ada")
#> [1] "Ada"
greet("Lovelace", title = "Ms")
#> [1] "Ms Lovelace"Constraining values
Beyond types, you can assert the content of a vector.
box::use(
assert[
assert_all_positive,
assert_between,
assert_values_in_set
]
)
assert_all_positive(c(1, 2, 3))
assert_between(c(0.2, 0.5, 0.9), lower = 0, upper = 1) # closed: [0, 1]
assert_between(c(0.2, 0.5, 0.9), lower = 0, upper = 1, lower_inclusive = FALSE) # ]0, 1]
assert_values_in_set(c("buy", "sell"), c("buy", "sell", "hold"))
invisible(NULL)assert_between() treats each bound as inclusive by
default; set lower_inclusive or
upper_inclusive to FALSE for an open end,
leave a bound NULL for a one-sided range, and pass
na_ok = TRUE to permit NA elements. It
compares with < / > only, so the same
call works for dates and date-times.
Relationships between arguments
When several optional arguments interact, the argument-group helpers
express the rule directly. They treat NULL as “not
supplied”.
box::use(assert[assert_exactly_one])
open_file <- function(path = NULL, connection = NULL) {
assert_exactly_one(path, connection)
return("ok")
}
open_file(path = "data.csv")
#> [1] "ok"
open_file() # neither supplied
#> Error in `open_file()`:
#> ! Exactly one of these must be supplied: path, connection (0 were
#> supplied).Stacking checks with the pipe
Because every assertion returns its input invisibly, checks compose
naturally with the native pipe |>. Each one validates
the value and passes it to the next, so a chain reads top to bottom as a
list of guarantees about the same object. This works for any object, and
is especially handy for data frames, where you often want to confirm
structure and column types in one place.
box::use(
assert[
assert_data_frame,
assert_has_columns,
assert_column_types,
assert_unique_rows
]
)
trades <- data.frame(
symbol = c("AAA", "BBB"),
quantity = c(10L, 5L),
price = c(1.5, 2.0)
)
trades |>
assert_data_frame() |>
assert_has_columns(c("symbol", "quantity", "price")) |>
assert_column_types("character", "symbol") |>
assert_column_types("integer", "quantity") |>
assert_column_types("numeric", "price") |>
assert_unique_rows() |>
invisible()One of several types
Stacking assertions means “all of these must hold”. When a value may
legitimately be one of several types or shapes,
assert_any_of() is the “or”: it accepts the value if
any of the listed checks passes, and on failure reports
every alternative it tried. Each alternative is a function taking the
value — an existing assertion by name, or a small closure stacking
several checks.
box::use(assert[assert_any_of, assert_numeric, assert_character])
identifier <- function(x) {
assert_any_of(x, assert_numeric, assert_character)
return(x)
}
identifier(42)
#> [1] 42
identifier("abc")
#> [1] "abc"
identifier(TRUE) # neither numeric nor character
#> Error in `identifier()`:
#> ! `x` must satisfy at least one of: `x` must be a numeric vector. | `x`
#> must be a character vector.The escape hatch
For any condition without a dedicated assertion, use
assert_true() with a custom message.
box::use(assert[assert_scalar_numeric, assert_true])
set_threshold <- function(x) {
assert_scalar_numeric(x)
assert_true(x %% 2 == 0, message = "`x` must be an even number.")
return(x)
}
set_threshold(4)
#> [1] 4
set_threshold(3)
#> Error in `set_threshold()`:
#> ! `x` must be an even number.