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
 bis 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: 0x7ff1d7542668>
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(). 
 

Share this post
Twitter
Google+
Facebook
Reddit
LinkedIn
StumbleUpon
Pinterest
Email