The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

Runtime Contracts for R Functions

Gilles Colling

2026-03-04

library(restrictR)

Overview

restrictR lets you define reusable input contracts from small building blocks using the base pipe |>. A contract is defined once and called like a function to validate data at runtime.

Section What you’ll learn
Reusable schemas Define and reuse data.frame contracts
Dependent validation Constraints that reference other arguments
Enum arguments Restrict string arguments to a fixed set
Custom steps Domain-specific invariants
Self-documentation Print, as_contract_text(), as_contract_block()
Using contracts in packages The recommended pattern for R packages

Reusable Schemas

The most common use case: validating a newdata argument in a predict-like function. Instead of scattering if/stop() blocks, define the contract 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)

The result is a callable function. Valid input passes silently:

good <- data.frame(x1 = c(1, 2, 3), x2 = c(4, 5, 6))
require_newdata(good)

Invalid input produces a structured error with the exact path and position:

require_newdata(42)
#> Error:
#> ! newdata: must be a data.frame, got numeric
require_newdata(data.frame(x1 = c(1, NA), x2 = c(3, 4)))
#> Error:
#> ! newdata$x1: must not contain NA
#>   At: 2
require_newdata(data.frame(x1 = c(1, 2), x2 = c("a", "b")))
#> Error:
#> ! newdata$x2: must be numeric, got character

Every error follows the same format: path: message, optionally followed by Found: and At: lines. This makes errors instantly recognizable and grep-friendly.

Dependent Validation

Some contracts depend on context. A prediction vector must have the same length as the rows in newdata:

require_pred <- restrict("pred") |>
  require_numeric(no_na = TRUE, finite = TRUE) |>
  require_length_matches(~ nrow(newdata))

The formula ~ nrow(newdata) declares a dependency on newdata. Pass it explicitly when calling the validator:

newdata <- data.frame(x1 = 1:5, x2 = 6:10)
require_pred(c(0.1, 0.2, 0.3, 0.4, 0.5), newdata = newdata)

Mismatched lengths produce a precise diagnostic:

require_pred(c(0.1, 0.2, 0.3), newdata = newdata)
#> Error:
#> ! pred: length must match nrow(newdata) (5)
#>   Found: length 3

Missing context is caught before any checks run:

require_pred(c(0.1, 0.2, 0.3))
#> Error:
#> ! `pred` depends on: newdata. Pass newdata = ... when calling the validator.

Context can also be passed as a named list via .ctx:

require_pred(1:5, .ctx = list(newdata = newdata))

Enum Arguments

For string arguments that must be one of a fixed set:

require_method <- restrict("method") |>
  require_character(no_na = TRUE) |>
  require_length(1L) |>
  require_one_of(c("euclidean", "manhattan", "cosine"))
require_method("euclidean")
require_method("chebyshev")
#> Error:
#> ! method: must be one of ["euclidean", "manhattan", "cosine"]
#>   Found: "chebyshev"
#>   At: 1

Custom Steps

For domain-specific invariants that don’t belong in the built-in set, use require_custom(). The step function receives (value, name, ctx) and should call stop() on failure:

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) {
        stop(sprintf("%s: must sum to 1, sums to %g", name, sum(value)),
             call. = FALSE)
      }
    }
  )
require_weights(c(0.5, 0.3, 0.2))
require_weights(c(0.5, 0.5, 0.5))
#> Error:
#> ! weights: must sum to 1, sums to 1.5

Custom steps can also declare dependencies:

require_probs <- restrict("probs") |>
  require_numeric(no_na = TRUE) |>
  require_custom(
    label = "length must match number of classes",
    deps = "n_classes",
    fn = function(value, name, ctx) {
      if (length(value) != ctx$n_classes) {
        stop(sprintf("%s: expected %d probabilities, got %d",
                     name, ctx$n_classes, length(value)), call. = FALSE)
      }
    }
  )

require_probs(c(0.3, 0.7), n_classes = 2L)

Self-Documentation

Print a validator to see its full contract:

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

Use as_contract_text() to generate a one-line summary for roxygen @param:

as_contract_text(require_newdata)
#> [1] "Must be a data.frame. must have columns: \"x1\", \"x2\". $x1 must be numeric (no NA, finite). $x2 must be numeric (no NA, finite). must have at least 1 row."

Use as_contract_block() for multi-line output suitable for @details:

cat(as_contract_block(require_newdata))
#> - must be a data.frame
#> - must have columns: "x1", "x2"
#> - $x1 must be numeric (no NA, finite)
#> - $x2 must be numeric (no NA, finite)
#> - must have at least 1 row

Using Contracts in Packages

The recommended pattern: define contracts in R/contracts.R, call them at the top of exported functions.

# R/contracts.R
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_pred <- restrict("pred") |>
  require_numeric(no_na = TRUE, finite = TRUE) |>
  require_length_matches(~ nrow(newdata))
# R/predict.R

#' Predict from a fitted model
#'
#' @param newdata Must be a data.frame. must have columns: "x1", "x2". $x1 must be numeric (no NA, finite). $x2 must be numeric (no NA, finite). must have at least 1 row.
#' @param ... additional arguments passed to the underlying model.
#'
#' @export
my_predict <- function(object, newdata, ...) {
  require_newdata(newdata)
  pred <- do_prediction(object, newdata)
  require_pred(pred, newdata = newdata)
  pred
}

Contracts compose naturally with the pipe and branch safely (each |> creates a new validator):

base <- restrict("x") |> require_numeric()
v1 <- base |> require_length(1L)
v2 <- base |> require_between(lower = 0)

# base is unchanged
length(environment(base)$steps)
#> [1] 1
length(environment(v1)$steps)
#> [1] 2
length(environment(v2)$steps)
#> [1] 2
sessionInfo()
#> R version 4.5.2 (2025-10-31 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26200)
#> 
#> Matrix products: default
#>   LAPACK version 3.12.1
#> 
#> locale:
#> [1] LC_COLLATE=C                          
#> [2] LC_CTYPE=English_United States.utf8   
#> [3] LC_MONETARY=English_United States.utf8
#> [4] LC_NUMERIC=C                          
#> [5] LC_TIME=English_United States.utf8    
#> 
#> time zone: Europe/Luxembourg
#> tzcode source: internal
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] restrictR_0.1.0
#> 
#> loaded via a namespace (and not attached):
#>  [1] digest_0.6.39     R6_2.6.1          fastmap_1.2.0     xfun_0.55        
#>  [5] glue_1.8.0        cachem_1.1.0      knitr_1.51        htmltools_0.5.9  
#>  [9] rmarkdown_2.30    lifecycle_1.0.5   cli_3.6.5         vctrs_0.7.1      
#> [13] svglite_2.2.2     sass_0.4.10       textshaping_1.0.4 jquerylib_0.1.4  
#> [17] systemfonts_1.3.1 compiler_4.5.2    tools_4.5.2       pillar_1.11.1    
#> [21] evaluate_1.0.5    bslib_0.9.0       yaml_2.3.12       otel_0.2.0       
#> [25] rlang_1.1.7       jsonlite_2.0.0

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.