Functional Input Validation

A set of basic tools to transform functions into functions with input validation checks, in a manner suitable for both programmatic and interactive use.


Development has moved to rong

BuildStatus codecov CRAN_Status_Badge stability-frozen License:MIT

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")

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

  • 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 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

Alternatively, use the lift() function from the purrr package:

bc_tri <- firmly(bc, list(outside ~ list(x, y)) ~  purrr::lift(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:

  • Precede the function header with input checks, by explicitly assigning the function to firmly()’s .f argument:

    bc <- firmly(
      ~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))
    #> FALSE: (function(.) {length(.) == 1L})(y)
     
    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)
     
    bc2 <- list(
      ~is.numeric,
      ~{length(.) == 1L},
      vld_true(outside ~ in_triangle(x, y))
    ) %>%
      firmly(function(x, y) {
        c(x, y, 1 - x - y)
      },
      .checklist = .)
     
    all.equal(bc, bc2)
    #> [1] TRUE
  • Better yet, use the %checkin% operator:

    bc3 <- list(
      ~is.numeric,
      ~{length(.) == 1L},
      vld_true(outside ~ in_triangle(x, y))
    ) %checkin%
      function(x, y) {
        c(x, y, 1 - x - y)
      }
     
    all.equal(bc, bc3)
    #> [1] TRUE

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

  • 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.

License

MIT Copyright © 2019 Eugene Ha

News

valaddin

1.0.0

With this release, valaddin is frozen. The next iteration of valaddin is the rong package. Future releases of valaddin itself will only include bug fixes.

  • Maintenance of the package has been substantially improved by dropping purrr as a dependency (#57, #58). The lazyeval package, whose features are frozen at 0.2.1, is now the only dependency.

Breaking changes

  • vld_singleton() now only checks whether an object has length 1, atomic or not.

  • Bare-vector checkers (e.g., vld_bare_logical()) have been removed.

0.1.2

Operator for input validation

  • A new operator %checkin% provides an alternative way to write checks next to function headers (#21, suggested by @MilkWasABadChoice). For example, writing list(~is.numeric, ~{. > 0}) %checkin% function(a, b) a + b is equivalent to writing firmly(~is.numeric, ~{. > 0}, .f = function(a, b) a + b). Using the %checkin% operator is the recommended way to apply input validation in scripts and packages.

New functions

  • The localized check makers vld_any() and vld_all() correspond to the base predicates any() and all().

  • To match naive expectations, a localized check maker vld_closure() is introduced to validate closures, i.e., non-primitive functions, while vld_function() has been redefined to validate functions in general, i.e., it corresponds to the base R predicate is.function (#18).

Minor improvements

  • dplyr is no longer required.

  • Since loosely() is typically used to obviate the overhead of input validation, calling it should itself impose as little overhead as possible (#28). Therefore, loosely() has been streamlined: it no longer checks its inputs. Calling it is now on par with calling firm_core().

  • firmly() gets a new option, .error_class, that enables you to customize the subclass of the error object that is signaled when an input validation error occurs (#25).

  • Reduced use of assignment, subsetting and warning suppression speeds up firmly to within an order of magnitude of the speed of input validation using stopifnot.

  • The environment of check formulae generated by the vld_*() check makers is the package namespace environment. (Previously, such check formulae got their own environment, though there was no need for such separation.)

  • Printing of functions (i.e., those underlying firmly applied functions and predicate functions of check-formula makers) is normalized to eliminate spurious indentation (#23).

  • Minor edits to vignette.

Bug fixes

  • Validation error messages now display all arguments with specified or default value (#33). Previously, only specified arguments were shown, even when the source of a validation failure was an invalid default value.

  • When evaluating input-validation expressions, the lexical scope of promises is now completely isolated from the lexical scope of (check formula) predicate functions (#32). With this fix, firmly() and %checkin% are now safe to use in package namespace environments. Previously, it was possible for a predicate function to be hijacked by a homonymous promise, or for an input validation to fail for an argument with default value, if that default value was inaccessible from the parent frame.

  • If a formal argument happened to coincide with the name of an object in the input validation procedure (valaddin:::validating_closure()), that formal argument could be inadvertently invoked in place of that object. This bug has been fixed by referencing bindings in the enclosing environment. (However, doing something truly ill-advised, such as duping a base R function, will still go unsupervised.) See commits abae548 and dcfdcaf.

  • Minor fixes to documentation, but not code, to ensure compatibility with purrr 0.2.3.

Deprecated

  • The .quiet argument has been dropped from loosely(). (If desired, the behavior of that option can be replicated by signaling an appropriate warning prior to calling loosely().)

0.1.0

  • First stable release:

    • Main functional operators: firmly, loosely
    • Component extractors: firm_checks, firm_core, firm_args
    • Check-scope converters (checker factories): localize, globalize
    • Localized base-R- and purrr-predicate checkers: vld_*
  • vld_numeric, vld_scalar_numeric are based on base::is.numeric, since the corresponding predicates in purrr will be deprecated starting from version 0.2.2.9000 (#12).

  • Fulfills aim of purrr proposal #275 (closed).

Reference manual

It appears you don't have a PDF plugin for this browser. You can click here to download the reference manual.

install.packages("valaddin")

1.0.0 by Eugene Ha, 4 months ago


https://github.com/egnha/valaddin


Report a bug at https://github.com/egnha/valaddin/issues


Browse source code at https://github.com/cran/valaddin


Authors: Eugene Ha [aut, cre]


Documentation:   PDF Manual  


MIT + file LICENSE license


Imports lazyeval

Suggests magrittr, testthat, stringr, knitr, rmarkdown


See at CRAN