Function operators

Learning objectives:

  • Define function operator
  • Explore some existing function operators
  • Make our own function operator

Introduction

  • A function operator is a function that takes one (or more) functions as input and returns a function as output.

  • Function operators are a special case of function factories, since they return functions.

  • They are often used to wrap an existing function to provide additional capability, similar to python’s decorators.

chatty <- function(f) {
  force(f)
  
  function(x, ...) {
    res <- f(x, ...)
    cat("Processing ", x, "\n", sep = "")
    res
  }
}

f <- function(x) x ^ 2
s <- c(3, 2, 1)

purrr::map_dbl(s, chatty(f))
#> Processing 3
#> Processing 2
#> Processing 1
#> [1] 9 4 1

Existing function operators

Two function operator examples are purrr:safely() and memoise::memoise(). These can be found in purr and memoise:

library(purrr)
library(memoise)

purrr::safely

Capturing Errors: turns errors into data!

x <- list(
  c(0.512, 0.165, 0.717),
  c(0.064, 0.781, 0.427),
  c(0.890, 0.785, 0.495),
  "oops"
)
map_dbl(x, sum)
#> Error in .Primitive("sum")(..., na.rm = na.rm): invalid 'type' (character) of
#> argument
# note use of map (not map_dbl), safely returns a lisst

out <- map(x, safely(sum))
str(transpose(out))
#> List of 2
#>  $ result:List of 4
#>   ..$ : num 1.39
#>   ..$ : num 1.27
#>   ..$ : num 2.17
#>   ..$ : NULL
#>  $ error :List of 4
#>   ..$ : NULL
#>   ..$ : NULL
#>   ..$ : NULL
#>   ..$ :List of 2
#>   .. ..$ message: chr "invalid 'type' (character) of argument"
#>   .. ..$ call   : language .Primitive("sum")(..., na.rm = na.rm)
#>   .. ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"

Other purrr function operators

purrr comes with three other function operators in a similar vein:

  possibly(): returns a default value when there’s an error. It provides no way to tell if an error occured or not, so it’s best reserved for cases when there’s some obvious sentinel value (like NA).

  quietly(): turns output, messages, and warning side-effects into output, message, and warning components of the output.

  auto_browser(): automatically executes browser() inside the function when there’s an error.

memoise::memoise

Caching computations: avoid repeated computations!

slow_function <- function(x) {
  Sys.sleep(1)
  x * 10 * runif(1)
}
system.time(print(slow_function(1)))
#> [1] 3.054764
#>    user  system elapsed 
#>    0.02    0.00    1.02
system.time(print(slow_function(1)))
#> [1] 3.644634
#>    user  system elapsed 
#>    0.00    0.00    1.03
fast_function <- memoise::memoise(slow_function)
system.time(print(fast_function(1)))
#> [1] 7.451108
#>    user  system elapsed 
#>    0.00    0.00    1.01
system.time(print(fast_function(1)))
#> [1] 7.451108
#>    user  system elapsed 
#>    0.00    0.00    0.01

Be careful about memoising impure functions!

Exercise

How does safely() work?
The source code looks like this:

safely
#> function (.f, otherwise = NULL, quiet = TRUE) 
#> {
#>     .f <- as_mapper(.f)
#>     force(otherwise)
#>     check_bool(quiet)
#>     function(...) capture_error(.f(...), otherwise, quiet)
#> }
#> <bytecode: 0x00000183867aacf0>
#> <environment: namespace:purrr>

The real work is done in capture_error which is defined in the package namespace. We can access it with the ::: operator. (Could also grab it from the function’s environment.)

purrr:::capture_error
#> function (code, otherwise = NULL, quiet = TRUE) 
#> {
#>     tryCatch(list(result = code, error = NULL), error = function(e) {
#>         if (!quiet) {
#>             message("Error: ", conditionMessage(e))
#>         }
#>         list(result = otherwise, error = e)
#>     })
#> }
#> <bytecode: 0x0000018386817b60>
#> <environment: namespace:purrr>

Case study: make your own function operator

urls <- c(
  "adv-r" = "https://adv-r.hadley.nz", 
  "r4ds" = "http://r4ds.had.co.nz/"
  # and many many more
)
path <- paste(tempdir(), names(urls), ".html")

walk2(urls, path, download.file, quiet = TRUE)

Here we make a function operator that add a little delay in reading each page:

delay_by <- function(f, amount) {
  force(f)
  force(amount)
  
  function(...) {
    Sys.sleep(amount)
    f(...)
  }
}
system.time(runif(100))
#>    user  system elapsed 
#>       0       0       0
system.time(delay_by(runif, 0.1)(100))
#>    user  system elapsed 
#>    0.00    0.00    0.11

And another to add a dot after nth invocation:

dot_every <- function(f, n) {
  force(f)
  force(n)
  
  i <- 0
  function(...) {
    i <<- i + 1
    if (i %% n == 0) cat(".")
    f(...)
  }
}

walk(1:100, dot_every(runif, 10))
#> ..........

Can now use both of these function operators to express our desired result:

walk2(
  urls, path, 
  download.file %>% dot_every(10) %>% delay_by(0.1), 
  quiet = TRUE
)

Exercise

  1. Should you memoise file.download? Why or why not?