| firmly | R Documentation |
firmly transforms a function into a function with input validation
checks. loosely undoes the application of firmly, by returning
the original function (without checks). is_firm is a predicate
function that checks whether an object is a firmly applied function, i.e.,
a function created by firmly.
Use %checkin% to apply firmly as an operator. Since this
allows you to keep checks and arguments adjacent, it is the preferred way to
use firmly in scripts and packages.
firmly(.f, ..., .checklist = list(), .warn_missing = character(),
.error_class = character())
.checks %checkin% .f
loosely(.f, .keep_check = FALSE, .keep_warning = FALSE)
is_firm(x)
.f |
Interpreted function, i.e., closure. |
... |
Input-validation check formula(e). |
.checklist |
List of check formulae. (These are combined with check
formulae provided via |
.warn_missing |
Arguments of |
.error_class |
Subclass of the error condition to be raised when an input validation error occurs (character). |
.checks |
List of check formulae, optionally containing character
vectors named |
.keep_check, .keep_warning |
Should existing checks, resp. missing-argument warnings, be kept? |
x |
Object to test. |
An input validation check is specified by a check formula, a special formula of the form
<scope> ~ <predicate>
where the right-hand side expresses what to check, and the left-hand side expresses where to check it.
The right-hand side <predicate> is a predicate function,
i.e, a one-argument function that returns either TRUE or
FALSE. It is the condition to check/enforce. The left-hand side
<scope> is an expression specifying what the condition is to be
applied to: whether the condition is to be applied to all
(non-...) arguments of .f (the case of “global
scope”), or whether the condition is to be selectively applied to certain
expressions of the arguments (the case of “local scope”).
According to scope, there are two classes of check formulae:
Check formulae of global scope
<string> ~ <predicate>
~<predicate>
\item \strong{Check formulae of local scope}
\preformatted{list(<check_item>, <check_item>, ...) ~ <predicate>}
A global check formula is a succinct way of asserting that the
function <predicate> returns TRUE when called on each
(non-...) argument of .f. Each argument for which
<predicate> fails—returns FALSE or is itself not
evaluable—produces an error message, which is auto-generated unless a
custom error message is supplied by specifying the string
<string>.
\subsection{Example}{
The condition that all (non-\code{\dots}) arguments of a function must
be numerical can be enforced by the check formula
\preformatted{~is.numeric}
or
\preformatted{"Not numeric" ~ is.numeric}
if the custom error message \code{"Not numeric"} is to be used (in lieu
of an auto-generated error message).
}
A local check formula imposes argument-specific conditions. Each
check item <check_item> is a formula of the form ~
<expression> (one-sided) or <string> ~ <expression>; it imposes
the condition that the function <predicate> is TRUE for the
expression <expression>. As for global check formulae, each check
item for which <predicate> fails produces an error message, which
is auto-generated unless a custom error message is supplied by a string
as part of the left-hand side of the check item (formula).
\subsection{Example}{
The condition that \code{x} and \code{y} must differ for the function
\code{function(x, y) {1 / (x - y)}} can be enforced by the local
check formula
\preformatted{list(~x - y) ~ function(.) abs(.) > 0}
or
\preformatted{list("x, y must differ" ~ x - y) ~ function(.) abs(.) > 0}
if the custom error message \code{"x, y must differ"} is to be used (in
lieu of an auto-generated error message).
}
Following the
magrittr
package, an anonymous (predicate) function of a single argument .
can be concisely expressed by enclosing the body of such a function
within curly braces { }.
\subsection{Example}{
The (onsided, global) check formula
\preformatted{~{. > 0}}
is equivalent to the check formula \code{~function(.) {. > 0}}
}
firmlyfirmly does nothing when there is nothing to do: .f is
returned, unaltered, when both .checklist and .warn_missing
are empty, or when .f has no named argument and
.warn_missing is empty.
Otherwise, \code{firmly} again returns a function that behaves
\emph{identically} to \code{.f}, but also performs input validation:
before a call to \code{.f} is attempted, its inputs are checked, and if
any check fails, an error halts further execution with a message
tabulating every failing check. (If all checks pass, the call to
\code{.f} respects lazy evaluation, as usual.)
\subsection{Subclass of the input-validation error object}{
The subclass of the error object is \code{.error_class}, unless
\code{.error_class} is \code{character()}. In the latter case, the
subclass of the error object is that of the existing error object, if
\code{.f} is itself a firmly applied function, or it is
\code{"simpleError"}, otherwise.
}
\subsection{Formal Arguments and Attributes}{
\code{firmly} preserves the attributes and formal arguments of
\code{.f} (except that the \code{"class"} attribute gains the component
\code{"firm_closure"}, unless it already contains it).
}
%checkin%%checkin% applies the check formula(e) in the list .checks
to .f, using firmly. The .warn_missing and
.error_class arguments of firmly may be specified as named
components of .checks.
looselyloosely returns .f, unaltered, when .f is not a
firmly applied function, or both .keep_check and
.keep_warning are TRUE.
Otherwise, \code{loosely} returns the underlying (original) function,
stripped of any input validation checks imposed by \code{firmly}, unless
one of the flags \code{.keep_check}, \code{.keep_warning} is switched on:
if \code{.keep_check}, resp. \code{.keep_warning}, is \code{TRUE},
\code{loosely} retains any existing checks, resp. missing-argument
warnings, of \code{.f}.
is_firmis_firm returns TRUE if x is a firmly applied
function (i.e., has class "firm_closure"), and FALSE,
otherwise.
firmly is enhanced by a number of helper functions:
To verify that a check formula is syntactically correct, use the
predicates is_check_formula, is_checklist.
To make custom check-formula generators, use
localize.
Pre-made check-formula generators are provided to facilitate
argument checks for types,
scalar objects, and
other common data structures and input
assumptions. These functions are prefixed by vld_, for
convenient browsing and look-up in editors and IDE's that support name
completion.
To access the components of a firmly applied function, use
firm_core, firm_checks,
firm_error, firm_args, (or simply
print the function to display its components).
## Not run:
dlog <- function(x, h) (log(x + h) - log(x)) / h
# Require all arguments to be numeric (auto-generated error message)
dlog_fm <- firmly(dlog, ~is.numeric)
dlog_fm(1, .1) # [1] 0.9531018
dlog_fm("1", .1) # Error: "FALSE: is.numeric(x)"
# Require all arguments to be numeric (custom error message)
dlog_fm <- firmly(dlog, "Not numeric" ~ is.numeric)
dlog_fm("1", .1) # Error: "Not numeric: `x`"
# Alternatively, "globalize" a localized checker (see ?localize, ?globalize)
dlog_fm <- firmly(dlog, globalize(vld_numeric))
dlog_fm("1", .1) # Error: "Not double/integer: `x`"
# Predicate functions can be specified anonymously or by name
dlog_fm <- firmly(dlog, list(~x, ~x + h, ~abs(h)) ~ function(x) x > 0)
dlog_fm <- firmly(dlog, list(~x, ~x + h, ~abs(h)) ~ {. > 0})
is_positive <- function(x) x > 0
dlog_fm <- firmly(dlog, list(~x, ~x + h, ~abs(h)) ~ is_positive)
dlog_fm(1, 0) # Error: "FALSE: is_positive(abs(h))"
# Describe checks individually using custom error messages
dlog_fm <-
firmly(dlog,
list("x not positive" ~ x, ~x + h, "Division by 0 (=h)" ~ abs(h)) ~
is_positive)
dlog_fm(-1, 0)
# Errors: "x not positive", "FALSE: is_positive(x + h)", "Division by 0 (=h)"
# Specify checks more succinctly by using a (localized) custom checker
req_positive <- localize("Not positive" ~ is_positive)
dlog_fm <- firmly(dlog, req_positive(~x, ~x + h, ~abs(h)))
dlog_fm(1, 0) # Error: "Not positive: abs(h)"
# Combine multiple checks
dlog_fm <- firmly(dlog,
"Not numeric" ~ is.numeric,
list(~x, ~x + h, "Division by 0" ~ abs(h)) ~ {. > 0})
dlog_fm("1", 0) # Errors: "Not numeric: `x`", check-eval error, "Division by 0"
# Any check can be expressed using isTRUE
err_msg <- "x, h differ in length"
dlog_fm <- firmly(dlog, list(err_msg ~ length(x) - length(h)) ~ {. == 0L})
dlog_fm(1:2, 0:2) # Error: "x, h differ in length"
dlog_fm <- firmly(dlog, list(err_msg ~ length(x) == length(h)) ~ isTRUE)
dlog_fm(1:2, 0:2) # Error: "x, h differ in length"
# More succinctly, use vld_true
dlog_fm <- firmly(dlog, vld_true(~length(x) == length(h), ~all(abs(h) > 0)))
dlog_fm(1:2, 0:2)
# Errors: "Not TRUE: length(x) == length(h)", "Not TRUE: all(abs(h) > 0)"
dlog_fm(1:2, 1:2) # [1] 0.6931472 0.3465736
# loosely recovers the underlying function
identical(loosely(dlog_fm), dlog) # [1] TRUE
# Use .warn_missing when you want to ensure an argument is explicitly given
# (see vignette("valaddin") for an elaboration of this particular example)
as_POSIXct <- firmly(as.POSIXct, .warn_missing = "tz")
Sys.setenv(TZ = "EST")
as_POSIXct("2017-01-01 03:14:16") # [1] "2017-01-01 03:14:16 EST"
# Warning: "Argument(s) expected ... `tz`"
as_POSIXct("2017-01-01 03:14:16", tz = "UTC") # [1] "2017-01-01 03:14:16 UTC"
loosely(as_POSIXct)("2017-01-01 03:14:16") # [1] "2017-01-01 03:14:16 EST"
# Use firmly to constrain undesirable behavior, e.g., long-running computations
fib <- function(n) {
if (n <= 1L) return(1L)
Recall(n - 1) + Recall(n - 2)
}
fib <- firmly(fib, list("`n` capped at 30" ~ ceiling(n)) ~ {. <= 30L})
fib(21) # [1] 17711 (NB: Validation done only once, not for every recursive call)
fib(31) # Error: `n` capped at 30
# Apply fib unrestricted
loosely(fib)(31) # [1] 2178309 (may take several seconds to finish)
# firmly won't force an argument that's not involved in checks
g <- firmly(function(x, y) "Pass", list(~x) ~ is.character)
g(c("a", "b"), stop("Not signaled")) # [1] "Pass"
# In scripts and packages, it is recommended to use the operator %checkin%
vec_add <- list(
~is.numeric,
list(~length(x) == length(y)) ~ isTRUE,
.error_class = "inputError"
) %checkin%
function(x, y) {
x + y
}
# Or call firmly with .f explicitly assigned to the function
vec_add2 <- firmly(
~is.numeric,
list(~length(x) == length(y)) ~ isTRUE,
.f = function(x, y) {
x + y
},
.error_class = "inputError"
)
all.equal(vec_add, vec_add2) # [1] TRUE
## End(Not run)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.