Description
openedon Dec 13, 2016
A common form of boilerplate code at the top of functions is argument checking: You make some checks on the arguments, signal a condition if any show-stopping checks fail, then move on to the meat of the function if everything is good. The problem with this approach is that it can clutter up the main work of a function with admin; it spoils the "fun" of a function with the inconvenience of a security check.
A function strictly()
, as a cohort of safely()
and possibly()
, would separate these concerns, functionally. It could have the signature
strictly(.f, ..., cond = NULL)
where .f
is an interpreted function and cond
is a condition object (if not NULL
). The main part is ...
, which consists of two-sided formulas p ~ m
that specify the argument checks for .f
:
p
is an expression that is a call to a predicate function (the "check")m
is an expression whose evaluation can be coerced to a string (the "failure message" if the check fails)
The value of strictly(.f, ...)
would be a function that applies .f
"strictly."
Example:
foo <- function(f, x) x - f(x)
is_trig <- function(f) any(map_lgl(c(cos, sin, tan), identical, y = f))
chk_trig <- is_trig(f) ~ "f not trigonometric"
foo_s <- strictly(foo, is.numeric(x) ~ "x not numeric", chk_trig)
foo_s(sin, 2) # [1] 1.090703
foo_s(log, 2) # Error in foo_s(log, 2) : f not trigonometric
foo_s(cos, "a") # Error in foo_s(cos, "a") : x not numeric
foo_s(log, "a") # Error in foo_s(log, "a") : f not trigonometric; x not numeric
(While the error messages could be more informative, the basic utility is clear.)
Here's a basic implementation, with a modicum of metaprogramming:
strictly <- function(.f, ..., cond = NULL) {
..cond <- cond %||% identity
..chks <- list(...)
.is_fml <- map_lgl(..chks, is_formula)
if (!(is_function(.f) && all(.is_fml))) {
stop("Invalid arguments for strictly()", call. = FALSE)
}
# Use two-sided `..` to make clashes with names in body(.f) improbable
.body <- substitute({
..env.. <- as.list(environment())
..is_pass.. <- map_lgl(chks, lazyeval::f_eval_lhs, data = ..env..)
if (!all(..is_pass..)) {
..msg.. <- chks[!..is_pass..] %>%
map_chr(~ as.character(lazyeval::f_eval_rhs(.x, ..env..))) %>%
paste(collapse = "; ")
stop(..cond(..msg..))
}
body
}, list(body = body(.f), chks = ..chks))
.f_strict <- eval(call("function", formals(.f), as.call(.body)))
environment(.f_strict) <- list2env(as.list(environment(.f), all.names = TRUE),
parent = parent.env(environment(.f)))
environment(.f_strict)$..cond <- ..cond
environment(.f_strict)$..chks <- ..chks
.f_strict
}
Alternatively, one could implement strictly()
as a function-composition. However, the above implementation has the advantage of preserving the argument signature of .f
, as well as its source code and environment.
Would strictly()
be a meaningful addition to purrr?