Body modification part 3: Syntax for body modification/function wrapping

The prior experiment on generating automatic runtime tests and the ensurer package used different methods of trying to wrap the original function so that it could be augmented with tests. What other ways are possible/sensible?

Python has solved this problem with decorators - they’re pretty neat, and look great.

None of what follows is neat, and all of these are going to be a lot of work for no real benefit other than seeing how they operate!

Original function and Expected function after modification

Original Function

orig_func <- function() {print("Goodbye")}

Expected function after modification

function() {
  { print("Hello") }
  { print("Goodbye") }
}

Idea 1: Function which takes a function and returns a function

This is the easiest case to write and the easiest to understand. It’s how most(?) functions which modify another function are called e.g. memoise::memoise()

#-----------------------------------------------------------------------------
#' Helper function to add new content to the start and end of a function.
#-----------------------------------------------------------------------------
add_print <- function(fun, new_prefix={print("Hello")}) {
  body(fun) <- rlang::call2('{', substitute(new_prefix), body(fun))
  fun
}

modify_function <- function(fun) {
  add_print(fun)
}
modify_function(orig_func)
## function () 
## {
##     {
##         print("Hello")
##     }
##     {
##         print("Goodbye")
##     }
## }

Idea 2: Two functions - meta-function creator and an infix operator

This ends up being so convoluted, I wonder why I ever thought of doing it.

  1. generate an empty function with the right call signature then add the new print()
    • Use rlang::new_function()
  2. add the original body to this with a custom infix operator
#-----------------------------------------------------------------------------
# Replase the body of the given `fun` with the concatenation of 
# body1 and body2
#-----------------------------------------------------------------------------
`%add_body%` <- function(fun, new_body) {
  body(fun) <- rlang::call2('{', body(fun), substitute(new_body))
  fun
}


rlang::new_function(args=alist(), body=quote({print("hello")})) %add_body% {
  print("Goodbye")
}
## function () 
## {
##     {
##         print("hello")
##     }
##     {
##         print("Goodbye")
##     }
## }

Idea 3: Function which takes entire original function body as an argument

The ensurer package does something similar to this - but much more nicely!

modify_function2 <- function(args, prefix_body=quote({print("hello")}), new_body) {
  total_body <- bquote({
    .(prefix_body)
    .(new_body)
  })
  rlang::new_function(args, body=total_body)
}

modify_function2(args=alist(), new_body = quote({print("Goodbye")}))
## function () 
## {
##     {
##         print("hello")
##     }
##     {
##         print("Goodbye")
##     }
## }
## <environment: 0x7ffbc5e37338>

Idea 4: Overriding the assignment operator: Attempt 1

I thought this would work, but you can’t actually do this if the symbol f doesn’t currently exist yet!

`modify_function3<-` <- function(fun, value) {
  # do something to the fun
}

modify_function3(f) <- orig_func
> modify_function3(f) <- orig_func
Error in modify_function3(f) <- orig_func : object 'f' not found

Idea 4: Overloading the assignment operator: Attempt 2 - the pmatch::bind() approach

I borrowed this idea from Thomas Mailund’s pmatch package.

By using the “subset assignment” operator [<- and a dummy class, we can delay the evaluation of the function name we want to assign to.

modify_function3 <- structure(NA, class = "modifier")

`[<-.modifier` <- function(dummy, funcname, value) {
  funcname <- as.character(substitute(funcname)) 
  assign(funcname, add_print(value), pos = .GlobalEnv)  # sorry!
}

modify_function3[new_func3] <- orig_func

new_func3
## function () 
## {
##     {
##         print("Hello")
##     }
##     {
##         print("Goodbye")
##     }
## }

Conclusion

  • I’ve found some really stupid ways to go about doing function modification.
  • Nothing is anywhere as neat as a python decorator.
  • Keeping it simple seems to be the nicest solution i.e.
orig_func <- function() { something }
new_func  <- modify_func(orig_func)
  • Overloading the assignment operator has possibilities though:
modified_func[new_func] <- orig_func