Use the force()!

Function Factories and Lazy Evaluation in R

Mike Irvine

Overview

  • The problem I was working on…
  • Why I ran into trouble
  • Why does R’s lazy evaluation matter?
  • What are function factories?
  • The role of force()
  • Examples from Hadley Wickham’s Advanced R

The problem I was working on…

  • Creating a simulation where a function is created and will be called later
  • The function defines the rate of some outcome which varies by population
  • I was trying to create these in a list…

My first attempt

sub_populations <- c("a","b","c")
rates <- c(1, 0.5, 2) 

rate_functions <- list()
for(i in seq_along(sub_populations)){
  rate <- rates[i]
  sub_population <- sub_populations[i]
  rate_function <- function(){
    rate
  }
  rate_functions[[sub_population]] <- rate_function
}

rate_functions[["a"]]()
[1] 2
rate_functions[["b"]]()
[1] 2
rate_functions[["c"]]()
[1] 2

What is a Function Factory?

A function factory is a function that returns another function.

power_factory <- function(exp) {
  function(x) x ^ exp
}

square <- power_factory(2)
square(4)
[1] 16

R is Lazy…

R uses lazy evaluation, meaning function arguments are not evaluated until needed.

f <- function(x) {
  10
}
f(stop("Error!"))  
[1] 10

…Which Can Be a Problem

make_stat_function <- function(stat) {
  function(x) stat(x)
}
mean_fun <- make_stat_function(mean)

The Problem

make_stat_function <- function(stat) {
  function(x) stat(x)
}
stat <- mean
s_mean <- make_stat_function(stat)
stat <- var
s_var <- make_stat_function(stat)

s_mean(0:10)  # returns 5?
[1] 11

Enter force()

Use force() to evaluate arguments immediately inside the factory.

The definition of force is simple…

force
function (x) 
x
<bytecode: 0x00000164c219d5b8>
<environment: namespace:base>

Why is this useful?

function_factory <- function(x){
    function(){
        x
    }
}
x <- 1
f1 <- function_factory(x)
rlang::env_print(f1)
<environment: 0x00000164c60481e8>
Parent: <environment: global>
Bindings:
• x: <lazy>

Why is this useful?

function_factory <- function(x){
    force(x)
    function(){
        x
    }
}
x <- 1
f1 <- function_factory(x)
rlang::env_print(f1)
<environment: 0x00000164c6f228b8>
Parent: <environment: global>
Bindings:
• x: <dbl>

Visualizing the Fix

make_stat_function <- function(stat) {
  force(stat)
  function(x) stat(x)
}
stat <- mean
s1 <- make_stat_function(stat)
stat <- var

s1(0:10)  
[1] 5

Fixing my problem…

sub_populations <- c("a","b","c")
rates <- c(1, 0.5, 2) 

rate_functions <- list()
for(i in seq_along(sub_populations)){
  rate <- rates[i]
  sub_population <- sub_populations[i]
  rate_function <- function(){
    rate
  }
  rate_functions[[sub_population]] <- rate_function
}

rate_functions[["a"]]()
[1] 2
rate_functions[["b"]]()
[1] 2
rate_functions[["c"]]()
[1] 2

Fixing my problem…

sub_populations <- c("a","b","c")
rates <- c(1, 0.5, 2) 

rate_function_factory <- function(rate){
  force(rate)
  function() rate
}
rate_functions <- list()
for(i in seq_along(sub_populations)){
  rate <- rates[i]

  sub_population <- sub_populations[i]
  rate_functions[[sub_population]] <- rate_function_factory(rate)
}

rate_functions[["a"]]()
[1] 1
rate_functions[["b"]]()
[1] 0.5
rate_functions[["c"]]()
[1] 2

Use in a statistical model

boot_model <- function(df, formula) {
  mod <- lm(formula, data = df)
  fitted <- unname(fitted(mod))
  resid <- unname(resid(mod))
  rm(mod)

  function() {
    fitted + sample(resid)
  }
} 

boot_mtcars2 <- boot_model(mtcars, mpg ~ wt)
head(boot_mtcars2())
[1] 21.00000 21.00000 24.19270 19.07515 20.19749 18.59311
head(boot_mtcars2())
[1] 20.67161 21.86952 25.03800 18.21963 16.61753 19.99431

Summary

  • R’s lazy evaluation can surprise you
  • Function factories are powerful but subtle
  • Using force() to ensure the function has the correct binding
  • Need to be careful to not accidentally create copies of large objects…

Thank you!

The Dark Side of the force() is a pathway to many abilities some consider to be unnatural

For more info see Hadley Wickham’s Advanced R