Validation of R objects with more consistency and flexibility than base ‘is’ functions, having predictable and well documented behavior. Provides simple ‘is’/‘are’/‘has’ predicates, and complex assertions for common input validation needs. Includes improved object comparison, prototypes, if-else chains, and pattern matching.
predicater has three main parts:
-
Simple predicates: functions to test the type/data (
is_*()) or attributes (has_*()) of an object. Always returning a logical scalar, but with vectorized versions (are_*()). They are better named and more consistent than base Ris.*()functions, while at the same time allowing the user more flexibility. -
Complex predicates and assertions: functions to test multiple properties of an object (
test_*()) and raise informative errors when any of the tests fails (assert_*()). They are useful for input validation, and allow lots of control, meeting most of the needs a developer can have. -
Objects comparison: a more flexible
base::identical(); a notion of prototype (see vctrs prototypes) expanded for any metadata/attributes; pattern matching, similar tobase::switch()but for any object, not just strings; and self-contained re-exports offuns::if_else()anddplyr::case_when().
Main contributions to the ecosystem
This package extends the consistency that rlang brings to the R ecosystem, by introducing predicates for types, attributes, and other properties missing in rlang.
The validation functions are an alternative to several other packages, currently two of the most prominent being checkmate and chk. This package offers more control over the tests, a more modern error reporting, and a clearer separation between testing the type/data of an object and its attributes – e.g. test_integer() for the type and values v.s. test_matrix() for the ‘dim’ and ‘dimnames’ attributes. At the same time, these packages are much more established and have a bigger commitment to performance than this one currently has.
Finally, the ‘comparison’ functions present useful additions to R logic: the pattern matching in match_*() functions seems like a long needed addition to the R language; the is_ptype() function generalizes testing the metadata of an object; and identical2(), if_else2(), and case_when2() are modern versions of very popular functions.
Main functions
predicater standardizes the naming and API of many is_*()/has_*()/are_*() functions, but the most novel ones include:
- Testing for attributes with
has_attrs_any/all/onlyand filtering withattrs_rmv/keep. - More consistent tests for
NA,NaN,Inf, and finite values, withis/are_na2(),is/are_finite(), andis_nan()/is_inf(), e.g. solvingis.na(NaN) #> TRUE. - Expanding the test for ‘any vector type’ to ‘any collection type’ with
is_collection(), possibly including environments, pairlists, expression vectors, and NULL. -
is_integer_like(), that organizes the common test for ‘integer-ish’ into two modes:"unbounded"- difference fromround(x)within a given tolerance; and"bounded"- unbounded + within the R integer range. Correctly dealing withNaNandInfvalues, while allowing control overNAvalues. - Testing for names with
has/are_names_valid(), having more control over empty, NA, duplicate and invalid names. - A more organized notion of ‘object’, merging S4/object bits, S4/object typeof’s and class attribute in
is_object_like()andobject_system(). - The compare functions cited in the previous section.
The test_*() and assert_*() suite introduces a ‘menu’ of sub-tests shared across functions, with a focus on flexibility, and a modern reporting of errors. Below is an example for validating an integer-like object.
library(predicater)
# Object to be validated:
x <- c(1.0, 2.0, 3.0 + 1e-100, 4.0, Inf)
# Defining the sub tests:
args <- list(
mode = "unbounded", # Equal to round(x), within a tolerance (will pass)
mode_tol = sqrt(.Machine$double.eps),
len = c(1, 10), # Length must be between 1 and 10 (will pass)
n_dup = 0, # No duplicate values allowed (will pass)
n_nan = c(0, -2), # At most length(x) - 2 NaN values (will pass)
n_na = c(0, 2, 3), # 0, 2, or 3 NA values (will pass)
n_inf = \(n, l) n/l <= 0.5, # n of Inf <= 50% of total (will pass)
range = c(-5, +5), # Values between -5 and 5 (will fail)
set = list(yes = c(1), no = c(0)), # Must contain 1, and not 0 (will pass)
sentinels = c("null"), # If not a numeric vector, x can be a NULL sentinel
sorted = "desc", # Must be sorted in descending order (will fail)
custom = NULL # Any user-given custom function
)
rlang::exec(test_integer, x, !!!args)
#> FALSE (not all tests passed)Normally one would call test_integer(x, ...), but I defined the arguments first for visualization purposes.
Note that all sub-tests about ‘the number/size of something’ (len, n_na, etc.) can have different specifications (a single value, a range, a set of values, or a custom function). They appear in many other test_*() functions, as well as set, sentinels, custom, etc.
For the validation, often one will call assert_integer(x, ...) to validate user input to his function. Note that assert_*() functions allow control over the conditions’ environment, argument name, and others.
my_fun <- function(x) {
rlang::exec(
assert_integer, x, !!!args,
env = rlang::current_env(), # Shows the error coming from `my_fun()`
short_circuit = FALSE, # Reports all sub-tests, beyond the first fail
args_cnd = list(class = "my_fun_error")
)
# ... code depending on x's integer properties
}
try(my_fun(x))## Error in my_fun(x) : `x` failed `assert_integer()`:
## ✔ (pass) sentinels: no sentinel values allowed.
## ✔ (pass) type : must pass `predicater::is_integer_like()`() in "unbounded"
## mode.
## ✔ (pass) len : length must be in range 1 to 10.
## ✔ (pass) n_na : #of NA values must be in set 0, 2, and 3.
## ✔ (pass) n_dup : #of duplicate values must be 0.
## ✔ (pass) n_nan : #of NaN values must be in range 0 to -2.
## ✔ (pass) n_inf : #of Inf values must satisfy a custom function.
## ✖ (fail) range : must be in range -5 to 5. Found Inf.
## ✖ (fail) set : must be in a custom set. Was not.
## ✖ (fail) sorted: must be in descending order. Did not.
##
## ℹ See `predicater::assert_integer()` and this condition's `rs_assert_error`
## attribute for details.The error describes each sub-test and the reason (if possible) for the failure. Tests not defined (e.g. custom = NULL) are not reported.
The assert_*() functions return x on success, so they can be used in pipelines:
mtcars$mpg |> # Some external data
tan() |> # Some transformation
assert_double(
range = c(-10, 10), sorted = "asc",
x_name = "tan(mpg)"
) |> # Validation
cos() |> # If passes, continue the pipeline
try()## Error in (function (...) : `tan(mpg)` failed `assert_double()`:
## ✔ (pass) sentinels: no sentinel values allowed.
## ✖ (fail) range : must be in range -10 to 10. Found -47.0730006551673.
## • (skip) sorted: skiped given failure.
## ✔ (pass) type : must pass `predicater::is_double()`.
##
## ℹ See `predicater::assert_double()` and this condition's `rs_assert_error`
## attribute for details.Note that with short_circuit = TRUE (the default), the first failing sub-test will raise an error, and the rest will be left as ‘(skip)’.
Installation
You can install the development version of predicater from GitHub with any of the following commands:
# install.packages(c("pak", "renv", "remotes"))
pak::pak("ricardo-semiao/predicater")
renv::install("ricardo-semiao/predicater")
remotes::install_github("ricardo-semiao/predicater")Development status
Currently, predicater is in an experimental stage. The API is not stable yet and will go through breaking changes.
All functions have examples, and they are validated in the test suite. Still, the tests are not extensive and the functions have not been battle-tested yet.
Currently, the functions do not verify their inputs, although they are well documented in the help pages.
In the future, I plan to substitute the underlying engines of many functions with C code to improve performance and have cleaner access to the underlying C representation of the R objects.
Some future additions are planned: - is_*_scalar() for all types of objects, that also allow for excluding the NA value. - Predicates for all items in the C data header of R objects, as is the case of has_object_bit(). - A reporting framework that allows the user to store the results of test_*() functions and branch from and report them in a more flexible way than directly raising an error.