Development has moved to rong
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.
Install from CRAN
install.packages("valaddin")
or get the development version from GitHub using the devtools package
# install.packages("devtools")
::install_github("egnha/valaddin", ref = "dev", build_vignettes = TRUE) devtools
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()
<- function(f, x, dx) (f(x + dx) - f(x)) / dx secant
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:
<- function(f, x, dx) {
secant_numeric 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
While this works, it’s not ideal, even in this simple situation, because
it’s inconvenient for interactive use at the console: you have to declare a new function, and give it a new name (or copy-paste the original function body)
it doesn’t catch all errors, only the first that occurs among the checks
you’re back to square one, if you later realize you need additional checks, or want to skip them altogether.
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
<- firmly(secant, list(~x, ~dx) ~ is.numeric)
secant
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:
<- firmly(secant, list(~x, ~dx) ~ {length(.) == 1L})
secant
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:
<- loosely(secant) # Retrieves the original function
secant <- firmly(secant, list(~x, ~dx) ~ {is.numeric(.) && length(.) == 1L})
secant
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)
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 a custom error message to clarify the purpose of a check:
<- function(x, y) c(x, y, 1 - x - y)
bc
# Check that `y` is positive
<- firmly(bc, list("(x, y) not in upper half-plane" ~ y) ~ {. > 0})
bc_uhp
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
Leave the left-hand side of a check formula blank to apply it to all arguments:
<- firmly(bc, ~is.numeric)
bc_num
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:
<- firmly(bc, "Not numeric" ~ is.numeric)
bc_num
bc_num(.5, ".2")
#> Error: bc_num(x = 0.5, y = ".2")
#> Not numeric: `y`
Use the isTRUE()
predicate to implement checks depending
on multiple arguments or, equivalently, the check maker
vld_true()
:
<- function(x, y) {x >= 0 && y >= 0 && 1 - x - y >= 0}
in_triangle <- "(x, y) not in triangle"
outside
<- firmly(bc, list(outside ~ in_triangle(x, y)) ~ isTRUE)
bc_tri
# Or more concisely:
<- firmly(bc, vld_true(outside ~ in_triangle(x, y)))
bc_tri
# 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
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:
Precede the function header with input checks, by explicitly
assigning the function to firmly()
’s .f
argument:
<- firmly(
bc ~is.numeric,
~{length(.) == 1L},
vld_true(outside ~ in_triangle(x, y)),
.f = function(x, y) {
c(x, y, 1 - x - y)
}
)
bc(.5, .2)
#> [1] 0.5 0.2 0.3
bc(.5, c(.2, .1))
#> Error: bc(x = 0.5, y = c(0.2, 0.1))
#> 1) FALSE: (function(.) {length(.) == 1L})(y)
#> 2) Error evaluating check (function (x) is.logical(x) && length(x) == 1L && !is.na(x) && x)(in_triangle(x, y)): 'length = 2' in coercion to 'logical(1)'
bc(".5", 1)
#> Error: bc(x = ".5", y = 1)
#> 1) FALSE: is.numeric(x)
#> 2) (x, y) not in triangle
Use the magrittr
%>%
operator to deliver input checks, by capturing them
as a list with firmly()
’s .checklist
argument:
library(magrittr)
<- list(
bc2 ~is.numeric,
~{length(.) == 1L},
vld_true(outside ~ in_triangle(x, y))
%>%
) firmly(function(x, y) {
c(x, y, 1 - x - y)
},.checklist = .)
Better yet, use the %checkin%
operator:
<- list(
bc3 ~is.numeric,
~{length(.) == 1L},
vld_true(outside ~ in_triangle(x, y))
%checkin%
) function(x, y) {
c(x, y, 1 - x - y)
}
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.
assertive, assertthat, and checkmate provide handy
collections of predicate functions that you can use in conjunction with
firmly()
.
argufy takes a different approach to input validation, using roxygen comments to specify checks.
ensurer and assertr provide a means
of validating function values. Additionally, ensurer provides an
experimental replacement for function()
that builds
functions with type-validated arguments.
typeCheck, together with Types for R, enables the creation of functions with type-validated arguments by means of special type annotations. This approach is orthogonal to that of valaddin: whereas valaddin specifies input checks as predicate functions with scope, typeCheck specifies input checks as arguments with type.
MIT Copyright © 2016–2023 Eugene Ha