10.5 Function factories + functionals

To finish off the chapter, I’ll show how you might combine functionals and function factories to turn data into many functions. The following code creates many specially named power functions by iterating over a list of arguments:

names <- list(
  square = 2, 
  cube = 3, 
  root = 1/2, 
  cuberoot = 1/3, 
  reciprocal = -1
)
funs <- purrr::map(names, power1)

funs$root(64)
#> [1] 8
funs$root
#> function(x) {
#>     x ^ exp
#>   }
#> <bytecode: 0x558261e8c8d0>
#> <environment: 0x5582648878f8>

This idea extends in a straightforward way if your function factory takes two (replace map() with map2()) or more (replace with pmap()) arguments.

One downside of the current construction is that you have to prefix every function call with funs$. There are three ways to eliminate this additional syntax:

  • For a very temporary effect, you can use with():

    with(funs, root(100))
    #> [1] 10

    I recommend this because it makes it very clear when code is being executed in a special context and what that context is.

  • For a longer effect, you can attach() the functions to the search path, then detach() when you’re done:

    attach(funs)
    #> The following objects are masked _by_ .GlobalEnv:
    #> 
    #>     cube, square
    root(100)
    #> [1] 10
    detach(funs)

    You’ve probably been told to avoid using attach(), and that’s generally good advice. However, the situation is a little different to the usual because we’re attaching a list of functions, not a data frame. It’s less likely that you’ll modify a function than a column in a data frame, so the some of the worst problems with attach() don’t apply.

  • Finally, you could copy the functions to the global environment with env_bind() (you’ll learn about !!! in Section 19.6). This is mostly permanent:

    rlang::env_bind(globalenv(), !!!funs)
    root(100)
    #> [1] 10

    You can later unbind those same names, but there’s no guarantee that they haven’t been rebound in the meantime, and you might be deleting an object that someone else created.

    rlang::env_unbind(globalenv(), names(funs))

You’ll learn an alternative approach to the same problem in Section 19.7.4. Instead of using a function factory, you could construct the function with quasiquotation. This requires additional knowledge, but generates functions with readable bodies, and avoids accidentally capturing large objects in the enclosing scope. We use that idea in Section 21.2.4 when we work on tools for generating HTML from R.

10.5.1 Exercises

  1. Which of the following commands is equivalent to with(x, f(z))?

    1. x$f(x$z).
    2. f(x$z).
    3. x$f(z).
    4. f(z).
    5. It depends.
  2. Compare and contrast the effects of env_bind() vs. attach() for the following code.

    funs <- list(
      mean = function(x) mean(x, na.rm = TRUE),
      sum = function(x) sum(x, na.rm = TRUE)
    )
    
    attach(funs)
    #> The following objects are masked from package:base:
    #> 
    #>     mean, sum
    mean <- function(x) stop("Hi!")
    detach(funs)
    
    env_bind(globalenv(), !!!funs)
    mean <- function(x) stop("Hi!") 
    env_unbind(globalenv(), names(funs))