Composable Runtime Contracts for R
Define your input rules once, enforce them everywhere, and get clear error messages when something breaks. Validators snap together with |>, branch safely for different use cases, and can generate their own documentation for use in roxygen (R's standard doc system).
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: -1You know the pattern: every exported function starts with the same if (!is.numeric(...)) stop(...) checks, copy-pasted and slowly drifting apart. When a contract changes, you hunt for every copy. Your users get "x must be numeric" from one function and "expected numeric input" from another.
With restrictR, you write each contract once as a pipe chain, call it like a function at the top of any method, and your users always get the same structured error format. When a rule changes, you change it in one place. Your @param docs can even pull their text straight from the validator, so documentation and enforcement stay in sync by construction.
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)require_pred <- restrict("pred") |>
require_numeric(no_na = TRUE) |>
require_length_matches(~ nrow(newdata))
# Context is explicit, never magic
require_pred(predictions, newdata = df)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
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
as_contract_text(require_newdata)
#> "Must be a data.frame. Must have columns: \"x1\", \"x2\".
#> $x1 must be numeric (no NA, finite). ..."# Install development version from GitHub
# install.packages("pak")
pak::pak("gcol33/restrictR")predict2 <- function(object, newdata, ...) {
require_newdata(newdata)
out <- predict(object, newdata = newdata)
require_pred(out, newdata = newdata)
out
}require_method <- restrict("method") |>
require_character(no_na = TRUE) |>
require_length(1L) |>
require_one_of(c("euclidean", "manhattan", "cosine"))
compute_distance <- function(x, y, method = "euclidean") {
require_method(method)
# ...
}require_survey <- restrict("survey") |>
require_df() |>
require_has_cols(c("age", "income", "status")) |>
require_col_numeric("age", no_na = TRUE) |>
require_col_between("age", lower = 0, upper = 150) |>
require_col_numeric("income", no_na = TRUE, finite = TRUE) |>
require_col_one_of("status", c("active", "inactive", "pending"))#' @param newdata `r as_contract_text(require_newdata)`For domain-specific invariants, require_custom() lets you write your own
check while keeping the same error format via 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() |
"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}
}