egnha / valaddin

Functional input validation to make R functions more readable and robust
Other
33 stars 1 forks source link
data-validation input-validation r type-safety

Development has moved to rong

valaddin

R-CMD-check CRAN_Status_Badge stability-frozen

Dealing with invalid function inputs is a chronic pain for R users, given R’s weakly typed nature. valaddin provides pain relief—a lightweight R package that enables you to transform an existing function into a function with input validation checks, in situ, in a manner suitable for both programmatic use and interactive sessions.

Installation

Install from CRAN

install.packages("valaddin")

or get the development version from GitHub using the devtools package

# install.packages("devtools")
devtools::install_github("egnha/valaddin", ref = "dev", build_vignettes = TRUE)

Why use valaddin

Fail fast—save time, spare confusion

You can be more confident your function works correctly, when you know its arguments are well-behaved. But when they aren’t, its better to stop immediately and bring them into line, than to let them pass and wreak havoc, exposing yourself to breakages or, worse, silently incorrect results. Validating the inputs of your functions is good defensive programming practice.

Suppose you have a function secant()

secant <- function(f, x, dx) (f(x + dx) - f(x)) / dx

and you want to ensure that the user (or some code) supplies numerical inputs for x and dx. Typically, you’d rewrite secant() so that it stops if this condition is violated:

secant_numeric <- function(f, x, dx) {
  stopifnot(is.numeric(x), is.numeric(dx))
  secant(f, x, dx)
}

secant_numeric(log, 1, .1)
#> [1] 0.9531018

secant_numeric(log, "1", ".1")
#> Error in secant_numeric(log, "1", ".1"): is.numeric(x) is not TRUE

The standard approach in R is problematic

While this works, it’s not ideal, even in this simple situation, because

valaddin rectifies these shortcomings

valaddin provides a function firmly() that takes care of input validation by transforming the existing function, instead of forcing you to write a new one. It also helps you by reporting every failing check.

library(valaddin)

# Check that `x` and `dx` are numeric
secant <- firmly(secant, list(~x, ~dx) ~ is.numeric)

secant(log, 1, .1)
#> [1] 0.9531018

secant(log, "1", ".1")
#> Error: secant(f = log, x = "1", dx = ".1")
#> 1) FALSE: is.numeric(x)
#> 2) FALSE: is.numeric(dx)

To add additional checks, just apply the same procedure again:

secant <- firmly(secant, list(~x, ~dx) ~ {length(.) == 1L})

secant(log, "1", c(.1, .01))
#> Error: secant(f = log, x = "1", dx = c(0.1, 0.01))
#> 1) FALSE: is.numeric(x)
#> 2) FALSE: (function(.) {length(.) == 1L})(dx)

Or, alternatively, all in one go:

secant <- loosely(secant)  # Retrieves the original function
secant <- firmly(secant, list(~x, ~dx) ~ {is.numeric(.) && length(.) == 1L})

secant(log, 1, .1)
#> [1] 0.9531018

secant(log, "1", c(.1, .01))
#> Error: secant(f = log, x = "1", dx = c(0.1, 0.01))
#> 1) FALSE: (function(.) {is.numeric(.) && length(.) == 1L})(x)
#> 2) FALSE: (function(.) {is.numeric(.) && length(.) == 1L})(dx)

Check anything using a simple, consistent syntax

firmly() uses a simple formula syntax to specify arbitrary checks—not just type checks. Every check is a formula of the form <where to check> ~ <what to check>. The “what” part on the right is a function that does a check, while the (form of the) “where” part on the left indicates where to apply the check—at which arguments or expressions thereof.

valaddin provides a number of conveniences to make checks for firmly() informative and easy to specify.

Use custom error messages

Use a custom error message to clarify the purpose of a check:

bc <- function(x, y) c(x, y, 1 - x - y)

# Check that `y` is positive
bc_uhp <- firmly(bc, list("(x, y) not in upper half-plane" ~ y) ~ {. > 0})

bc_uhp(.5, .2)
#> [1] 0.5 0.2 0.3

bc_uhp(.5, -.2)
#> Error: bc_uhp(x = 0.5, y = -0.2)
#> (x, y) not in upper half-plane

Easily apply a check to all arguments

Leave the left-hand side of a check formula blank to apply it to all arguments:

bc_num <- firmly(bc, ~is.numeric)

bc_num(.5, ".2")
#> Error: bc_num(x = 0.5, y = ".2")
#> FALSE: is.numeric(y)

bc_num(".5", ".2")
#> Error: bc_num(x = ".5", y = ".2")
#> 1) FALSE: is.numeric(x)
#> 2) FALSE: is.numeric(y)

Or fill in a custom error message:

bc_num <- firmly(bc, "Not numeric" ~ is.numeric)

bc_num(.5, ".2")
#> Error: bc_num(x = 0.5, y = ".2")
#> Not numeric: `y`

Check conditions with multi-argument dependencies

Use the isTRUE() predicate to implement checks depending on multiple arguments or, equivalently, the check maker vld_true():

in_triangle <- function(x, y) {x >= 0 && y >= 0 && 1 - x - y >= 0}
outside <- "(x, y) not in triangle"

bc_tri <- firmly(bc, list(outside ~ in_triangle(x, y)) ~ isTRUE)

# Or more concisely:
bc_tri <- firmly(bc, vld_true(outside ~ in_triangle(x, y)))

# Or more concisely still, by relying on an auto-generated error message:
# bc_tri <- firmly(bc, vld_true(~in_triangle(x, y)))

bc_tri(.5, .2)
#> [1] 0.5 0.2 0.3

bc_tri(.5, .6)
#> Error: bc_tri(x = 0.5, y = 0.6)
#> (x, y) not in triangle

Make your code more intelligible

To make your functions more intelligible, declare your input assumptions and move the core logic to the fore. You can do this using firmly(), in several ways:

Learn more

See the package documentation ?firmly, help(p = valaddin) for detailed information about firmly() and its companion functions, and the vignette for an overview of use cases.

Related packages

License

MIT Copyright © 2016–2023 Eugene Ha