the same input check, written once
Composable runtime contracts for R, built from base pipe chains into immutable validator closures.
Write each input contract once. restrictR turns a |> chain of small require_*()
steps into a callable validator you drop at the top of any function. Every |>
returns a new validator, so you branch from a shared base without touching it, and
every failure comes back in one structured path: message format. No DSL, no operator
overloading; a validator is an ordinary R closure you can pass around, print, and read
back as documentation.
library(restrictR)
# define once
require_positive_scalar <- restrict("x") |>
require_numeric(no_na = TRUE) |>
require_length(1L) |>
require_between(lower = 0, exclusive_lower = TRUE)
# enforce anywhere
require_positive_scalar(3.14) # passes silently
require_positive_scalar(-1) # Error: x: must be in (0, Inf]
# Found: -1Every exported function tends to start with the same if (!is.numeric(...)) stop(...)
checks, copied across methods and drifting apart, until one function says "x must be numeric" and another says "expected numeric input". Define the contract once as a
pipe chain, call it at the top of any method, and the rule lives in one place with one
error format. Change the rule, change it once.
require_newdata <- restrict("newdata") |>
require_df() |>
require_has_cols(c("x1", "x2")) |>
require_col_numeric("x1", no_na = TRUE, finite = TRUE) |>
require_col_numeric("x2", no_na = TRUE, finite = TRUE) |>
require_nrow_min(1L)
predict2 <- function(object, newdata, ...) {
require_newdata(newdata)
predict(object, newdata = newdata)
}A one-sided formula references another argument by name. Context is passed explicitly when you call the validator, so evaluation never reaches into parent frames for it.
require_pred <- restrict("pred") |>
require_numeric(no_na = TRUE) |>
require_length_matches(~ nrow(newdata))
require_pred(predictions, newdata = df)Failures report the exact path and position, in one shared format:
newdata$x2: must be numeric, got character
pred: length must match nrow(newdata) (100)
Found: length 50
x: must not contain NA
At: 2, 5, 9
The same step list that runs the checks also prints the contract and renders it
as text for roxygen, so @param documentation and enforcement stay in sync:
print(require_newdata)
#> <restriction newdata>
#> 1. must be a data.frame
#> 2. must have columns: "x1", "x2"
#> 3. $x1 must be numeric (no NA, finite)
#> 4. $x2 must be numeric (no NA, finite)
#> 5. must have at least 1 row
#' @param newdata `r as_contract_text(require_newdata)`When a contract needs a domain-specific invariant, require_custom() runs your
own check while keeping the same error format through fail():
require_weights <- restrict("weights") |>
require_numeric(no_na = TRUE) |>
require_between(lower = 0, upper = 1) |>
require_custom(
label = "must sum to 1",
fn = function(value, name, ctx) {
if (abs(sum(value) - 1) > 1e-8) {
fail(name, "must sum to 1", found = sprintf("sum = %g", sum(value)))
}
}
)| Category | Steps |
|---|---|
| Type checks | require_df(), require_numeric(), require_integer(), require_character(), require_logical() |
| Null / missingness | require_not_null(), require_no_na(), require_finite() |
| Structure | require_scalar(), require_named(), require_length(), require_length_min(), require_length_max(), require_length_matches(), require_nrow_min(), require_nrow_matches(), require_has_cols() |
| Values | require_positive(), require_negative(), require_between(), require_one_of(), require_unique() |
| Columns | require_col_numeric(), require_col_character(), require_col_between(), require_col_one_of() |
| Extension | require_custom() |
checkmate is a fast, C-backed
toolkit for argument checking. It offers assert*, check*, test*, and
expect* families so that, inside a function, you write one call per argument:
assertNumeric(x, lower = 0, len = 1) either passes or raises an error on the
spot. Checks are expressed as direct function calls and run where they are
written.
restrictR works one level up. Instead of calling checks inline, you build a
named validator once as a |> chain of require_*() steps and reuse that object
across every function that takes the same argument. The two packages differ along
a few axes:
- Reuse. A
restrictRvalidator is a callable closure you define once and call at the top of many functions, so the rule fornewdatalives in one place. checkmate checks are written inline at each call site. - Composition. Validators compose with the base pipe and branch immutably:
base |> require_length(1L)andbase |> require_between(lower = 0)share a base without modifying it. checkmate composes by listing severalassert*calls in sequence. - Self-documentation. A validator prints its own contract, and
as_contract_text()renders it for a roxygen@param, so enforcement and documentation stay in sync. - Dependent rules. Cross-argument constraints use one-sided formulas
(
require_length_matches(~ nrow(newdata))) with context passed explicitly at call time. checkmate expresses such relationships with ordinary R between the checks. - Dependencies. checkmate uses compiled C code for speed.
restrictRis pure base R with zero runtime dependencies.
If you want fast, inline per-argument assertions, checkmate is a mature and
well-tested choice. If you want to name a contract once, reuse it across
functions, and have it document itself, that is what restrictR is built for.
The two also coexist: a require_custom() step can call a checkmate assertion
inside it.
install.packages("restrictR") # CRAN
install.packages("pak") # development version
pak::pak("gcol33/restrictR")"Software is like sex: it's better when it's free." -- Linus Torvalds
I'm a PhD student who builds R packages in my free time because I believe good tools should be free and open. I started these projects for my own work and figured others might find them useful too.
If this package saved you some time, buying me a coffee is a nice way to say thanks. It helps with my coffee addiction.
MIT (see the LICENSE file)
@software{restrictR,
author = {Colling, Gilles},
title = {restrictR: Composable Runtime Contracts for R},
year = {2026},
url = {https://github.com/gcol33/restrictR}
}