# Body modification part 4: Adding runtime checks to an existing function

## 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?

2. The original function may be in a package or not easily editable for other reasons.
3. You want to avoid manually writing a wrapper that calls the original function, and also avoid having to ensure the function signatures match.
4. 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")
}

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

# 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().