Problem: Can I augment an existing function with checking code for a function?
I’m still experimenting with function body modification, and I’ve downscaled my ambitions and thought about what a type check helper should look like.
My first attempt worked, but in a very roundabout fashion and had holes big enough to drive a truck through.
The previous post showed that the code to apply a function to another can get very obscure (and unreadable!). Keeping it simple makes it easier to understand and reason about.
I’ve decided for my purposes, functions-which-modify-functions should:
- Be very R-like, and avoid introducing new syntax (via infix operators or overloading the assignment operator) just for the sake of it.
- Use
memoise::memoise()
as a reasonable template for how they are called.
Why write a function to add checks to another function at all?
- To learn about functions.
- The original function may be in a package or not easily editable for other reasons.
- You want to avoid manually writing a wrapper that calls the original function, and also avoid having to ensure the function signatures match.
- You want to avoid writing all the boilerplate checks at the top of a function (for clarity)
It’s really only Point 1 that is motivating me for now.
Example function to be checked
orig_func <- function(a = 1L, b = 2, c='hello', ...) {
cat(c, a+b, "\n")
}
add_checks()
The function for adding checks to another function is shown below.
It operates by:
- taking a function as its first argument
- all subsequent arguments are interpreted as boolean statements for checking the arguments
- create a code block combining all these tests
- add this code block above the body of the original function
- return the augmented version of the function
#-----------------------------------------------------------------------------
#' add checks to an existing function and return a new function
#'
#' @param fun existing function passed in a symbol
#' @param ... list of checks to add in front of function body
#'
#' @return new function (with the same function signature as `fun`) and the same
#' body as `fun` with a block of tests inserted at the start of the function
#-----------------------------------------------------------------------------
add_checks <- function(fun, ...) {
# Capture all the tests and turn each one into a stopifnot() call
checks <- rlang::exprs(...)
for (i in seq(checks)) {
checks[[i]] <- bquote(stopifnot(isTRUE(.(checks[[i]]))))
}
# Bind all these checks into a single block
checks <- rlang::call2('{', splice(checks))
# Concatentate the test block with the original body
new_body <- bquote({
.(checks)
.(body(fun))
})
# create and return the new function
rlang::new_function(args = formals(fun), body = new_body)
}
The following code creates an enhanced version of orig_func()
which tests the following before execution:
- a is an integer
b
is non-negative- No more than 2 non-captured arguments are absorbed into the
...
construct
new_func <- add_checks(orig_func, is_integer(a), b >= 0, length(list(...)) < 3)
new_func
## function (a = 1L, b = 2, c = "hello", ...)
## {
## {
## stopifnot(isTRUE(is_integer(a)))
## stopifnot(isTRUE(b >= 0))
## stopifnot(isTRUE(length(list(...)) < 3))
## }
## {
## cat(c, a + b, "\n")
## }
## }
## <environment: 0x7f88f5422400>
new_func()
new_func(a = 1.1)
new_func(b = -1)
new_func(a = 1L, d = 1, e = 1, f=1)
> new_func()
hello 3
> new_func(a = 1.1)
Error: is_integer(a) is not TRUE
> new_func(b = -1)
Error: b >= 0 is not TRUE
> new_func(a = 1L, d = 1, e = 1, f=1)
Error: length(list(...)) < 3 is not TRUE
Conclusion
- This is much less insane than the first attempt at adding checks to an existing function
- Extensions to this idea:
- Output more informative error messages e.g. include the function name in the stop message.
- Add verification that the result of each test is a single, non-missing, non-NA boolean value.
- Don’t just exit at the first error - instead, display all possible errors before stopping execution.
- Write a similar function to check the return value of a function e.g.
add_return_value_check()
.