The rong approach to wrong inputs. Two (w)rongs make a right.
rong is a complete reimplementation of valaddin that supports tidyverse semantics.
Since this is a major break in the API of the current CRAN version of valaddin (which appears to have no reverse dependencies), it make sense to start from a clean slate and distinguish this reconception with a new name.
Dealing with invalid function inputs is a chronic pain for R users, given R’s weakly typed nature. rong provides pain relief in the form of an adverb, firmly()
, that enables you to transform an existing function into a function with input validation checks, in a manner suitable for both programmatic and interactive use.
Additionally, rong provides:
-
fasten()
, to help you write cleaner and more explicit function declarations in your scripts, by providing a functional operator that “fastens” a given set of input validations to functions (i.e., it curriesfirmly()
) -
validate()
, as syntactic sugar to validate objects, by applying input validation to the identity function -
loosely()
, to undo the application of input validation checks, at any time, by returning the original function
These functions support tidyverse semantics such as quasiquotation and splicing, to provide a flexible yet simple grammar for input validations.
# install.packages("devtools")
devtools::install_github("egnha/rong")
To illustrate rong’s functional approach to input validation, consider the function that computes the barycentric coordinates of a point in the plane:
bc <- function(x, y) {
c(x, y, 1 - x - y)
}
Imagine applying bc()
“firmly,” exactly as before, but with the assurance that the inputs are indeed numeric. To enable this, transform bc()
using firmly()
, relative to the validation specified by the predicate function is.numeric()
:
library(rong)
bc2 <- firmly(bc, is.numeric)
bc2(.5, .2)
#> [1] 0.5 0.2 0.3
bc2(.5, ".2")
#> Error: bc2(x = 0.5, y = ".2")
#> FALSE: is.numeric(y)
Using the string-interpolation syntax provided by the glue package, make error messages more informative, by taking into account the context of an error:
bc3 <- firmly(bc, "{{.}} is not numeric (type: {typeof(.)})" := is.numeric)
bc3(.5i, ".2")
#> Error: bc3(x = 0+0.5i, y = ".2")
#> 1) x is not numeric (type: complex)
#> 2) y is not numeric (type: character)
rong supports quasiquotation and splicing semantics for specifying input validation checks. Checks and (custom) error messages are captured as quosures, to ensure that validations, and their error reports, are hygienically evaluated in the intended scope—transparently to the user.
z <- 0
in_triangle <- vld_spec(
"{{.}} is not positive (value is {.})" :=
{isTRUE(. > !! z)}(x, y, 1 - x - y)
)
bc4 <- firmly(bc, is.numeric, !!! in_triangle)
bc4(.5, .2)
#> [1] 0.5 0.2 0.3
bc4(.5, .6)
#> Error: bc4(x = 0.5, y = 0.6)
#> 1 - x - y is not positive (value is -0.1)
This reads as follows:
-
vld_spec()
encapsulates the condition thatx
,y
,1 - x - y
are positive, as a formula definition. The predicate itself is succinctly expressed using magrittr’s shorthand for anonymous functions. The unquoting operator!!
ensures that the value ofz
is “burned into” the check. -
The additional condition that
(x, y)
lies in a triangle is imposed by splicing it in with the!!!
operator.
Validating an object (say, a data frame) is nothing other than applying an input-validated identity function to it. The function validate()
provides a shorthand for this.
# All assumptions OK, mtcars returned invisibly
validate(mtcars,
is.data.frame,
chk_lt(100, nrow(.)),
chk_has_names(c("mpg", "cyl")))
validate(mtcars,
is.data.frame,
chk_gt(100, nrow(.)),
chk_has_name("cylinders"))
#> Error: validate(. = mtcars)
#> 1) nrow(.) is not greater than 100
#> 2) . does not have name "cylinders"
Instead of writing
bc_cluttered <- function(x, y) {
if (!is.numeric(x) || length(x) != 1)
stop("x is not a number")
if (!is.numeric(y) || length(y) != 1)
stop("y is not a number")
if (!isTRUE(x > 0))
stop("x is not positive")
if (!isTRUE(y > 0))
stop("y is not in the upper-half plane")
if (!isTRUE(1 - x - y > 0))
stop("1 - x - y is not positive")
c(x, y, 1 - x - y)
}
use fasten()
to highlight the core logic, while keeping input assumptions in sight:
bc_clean <- fasten(
"{{.}} is not a number" := {is.numeric(.) && length(.) == 1},
"{{.}} is not positive" :=
{isTRUE(. > 0)}(x, "y is not in the upper-half plane" := y, 1 - x - y)
)(
function(x, y) {
c(x, y, 1 - x - y)
}
)
bc_clean(.5, .2)
#> [1] 0.5 0.2 0.3
bc_clean(c(.5, .5), -.2)
#> Error: bc_clean(x = c(0.5, 0.5), y = -0.2)
#> 1) x is not positive
#> 2) 1 - x - y is not positive
#> 3) y is not in the upper-half plane
#> 4) x is not a number
In addition to having cleaner code, you can:
-
reduce duplication, by using the splicing operator
!!!
to reuse common input validations -
recover the underlying “lean” function, at any time, using
loosely()
:loosely(bc_clean) #> function(x, y) { #> c(x, y, 1 - x - y) #> }
-
rong provides a basic set of predicate functions—prefixed
chk_
for easy lookup—to specify common kinds of checks, e.g., type and property checks, comparisons, etc.To enrich rong’s vocabulary of predicate functions, use:
-
specialized collections of predicate functions, such as assertive, assertthat, checkmate
-
vetr, which provides a concise declarative syntax to create custom predicate functions
-
-
Other non-functional approaches to input validation: argufy, ensurer, typeCheck
rong makes essential use of the rlang package by Lionel Henry and Hadley Wickham, which provides the engine for quasiquotation and expression capture. The glue package by Jim Hester enables string interpolation of error messages.
MIT Copyright © 2017 Eugene Ha