Sectioning a function

Søren Højsgaard

Section a functions domain: with section_fun()

The function section_fun is used to create a new function that is a section of the original function. The section is defined by fixing some of the arguments of the original function. The new function is defined on the remaining arguments.

For example, let \(f(x,y)=x+y\). Then \(f_x(y)=f(10, y)\) is a section of \(f\) to be a function of \(y\) alone.

More generally, let \(E\) be a subset of the cartesian product \(X \times Y\) where \(X\) and \(Y\) are some sets. Consider a function \(f(x,y)\) defined on \(E\). Then for any \(x \in X\), the section of \(E\) defined by \(x\) (denoted \(E_x\)) is the set of \(y\)’s in \(Y\) such that \((x, y)\) is in \(E\), i.e. $$ E_x = { y \in Y | (x,y) \in E} $$

Correspondingly, the section of \(f(x,y)\) defined by \(x\) is the function \(f_x\) defined on \(E_x\) given by \(f_x(y)=f(x,y)\).

The section_fun function in doBy

The function section_fun is used to create a new function that is a section of the original function. The section is defined by fixing some of the arguments of the original function. The new function is defined on the remaining arguments. There are the following approaches:

  1. insert the section values as default values in the function definition (default),
  2. insert the section values in the function body,
  3. store the section values in an auxillary environment.

Consider this function:

fun  <- function(a, b, c=4, d=9){
    a + b + c + d
}
fun_def <- section_fun(fun, list(b=7, d=10))
fun_def
#> function (a, c = 4, b = 7, d = 10) 
#> {
#>     a + b + c + d
#> }
fun_body <- section_fun(fun, list(b=7, d=10), method="sub")
fun_body
#> function (a, c = 4) 
#> {
#>     b = 7
#>     d = 10
#>     a + b + c + d
#> }
fun_env <- section_fun(fun, list(b=7, d=10), method = "env")
fun_env
#> function (a, c = 4) 
#> {
#>     . <- "use get_section(function_name) to see section"
#>     . <- "use get_fun(function_name) to see original function"
#>     args <- arg_getter()
#>     do.call(fun, args)
#> }
#> <environment: 0x5932076e2240>

In the last case, we can see the section and the original function definition as:

get_section(fun_env) 
#> $b
#> [1] 7
#> 
#> $d
#> [1] 10
## same as: attr(fun_env, "arg_env")$args 
get_fun(fun_env) 
#> <srcref: file "" chars 1:9 to 3:1>
## same as: environment(fun_env)$fun

We get:

fun(a=10, b=7, c=5, d=10)
#> [1] 32
fun_def(a=10, c=5)
#> [1] 32
fun_body(a=10, c=5)
#> [1] 32
fun_env(a=10, c=5)
#> [1] 32

Example: benchmarking

Consider a simple task: Creating and inverting Toeplitz matrices for increasing dimensions:

n <- 4
toeplitz(1:n)
#>      [,1] [,2] [,3] [,4]
#> [1,]    1    2    3    4
#> [2,]    2    1    2    3
#> [3,]    3    2    1    2
#> [4,]    4    3    2    1

An implementation is

inv_toep <- function(n) {
    solve(toeplitz(1:n))
}
inv_toep(4)
#>      [,1] [,2] [,3] [,4]
#> [1,] -0.4  0.5  0.0  0.1
#> [2,]  0.5 -1.0  0.5  0.0
#> [3,]  0.0  0.5 -1.0  0.5
#> [4,]  0.1  0.0  0.5 -0.4

We can benchmark timing for different values of \(n\) as

library(microbenchmark)
microbenchmark(
    inv_toep(4), inv_toep(8), inv_toep(16),
    inv_toep(32), inv_toep(64),
    times=5
)
#> Unit: microseconds
#>          expr   min    lq  mean median    uq    max neval cld
#>   inv_toep(4)  13.9  14.1  14.5   14.2  14.3   15.9     5   a
#>   inv_toep(8)  16.5  17.3  17.8   17.4  18.2   19.4     5   a
#>  inv_toep(16)  22.6  22.8  25.5   23.5  24.6   34.1     5   a
#>  inv_toep(32)  51.3  51.5 399.8   52.1  56.8 1787.4     5   a
#>  inv_toep(64) 156.8 159.4 164.2  162.4 164.4  178.2     5   a

However, it is tedious (and hence error prone) to write these function calls.

A programmatic approach using \code{section_fun} is as follows: First create a list of sectioned functions:

n.vec  <- c(3, 4, 5)
fun_list <- lapply(n.vec,
                  function(ni){
                      section_fun(inv_toep, list(n=ni))}
                  )

We can inspect and evaluate each / all functions as:

fun_list[[1]]
#> function (n = 3) 
#> {
#>     solve(toeplitz(1:n))
#> }
fun_list[[1]]()
#>        [,1] [,2]   [,3]
#> [1,] -0.375  0.5  0.125
#> [2,]  0.500 -1.0  0.500
#> [3,]  0.125  0.5 -0.375

To use the list of functions in connection with microbenchmark we bquote all functions using

bquote_list <- function(fnlist){
    lapply(fnlist, function(g) {
        bquote(.(g)())
    }
    )
}

We get:

bq_fun_list <- bquote_list(fun_list)
bq_fun_list[[1]]
#> (function (n = 3) 
#> {
#>     solve(toeplitz(1:n))
#> })()
## Evaluate one:
eval(bq_fun_list[[1]])
#>        [,1] [,2]   [,3]
#> [1,] -0.375  0.5  0.125
#> [2,]  0.500 -1.0  0.500
#> [3,]  0.125  0.5 -0.375
## Evaluate all:
## lapply(bq_fun_list, eval)

To use microbenchmark we must name the elements of the list:

names(bq_fun_list) <- n.vec
microbenchmark(
  list  = bq_fun_list,
  times = 5
)
#> Unit: microseconds
#>  expr  min   lq mean median   uq  max neval cld
#>     3 13.2 13.3 28.2   13.5 13.9 87.0     5   a
#>     4 13.2 13.6 16.7   13.8 17.9 25.0     5   a
#>     5 13.6 14.2 16.8   14.9 17.6 23.5     5   a

Running the code below provides a benchmark of the different ways of sectioning in terms of speed.

n.vec  <- seq(20, 80, by=20)
fun_def <- lapply(n.vec,
                  function(ni){
                      section_fun(inv_toep, list(n=ni), method="def")}
                  )
fun_body <- lapply(n.vec,
                  function(ni){
                      section_fun(inv_toep, list(n=ni), method="sub")}
                  )
fun_env <- lapply(n.vec,
                  function(ni){
                      section_fun(inv_toep, list(n=ni), method="env")}
                  )

bq_fun_list <- bquote_list(c(fun_def, fun_body, fun_env))
names(bq_fun_list) <- paste0(rep(c("def", "body", "env"), each=length(n.vec)), rep(n.vec, 3))

mb <- microbenchmark(
  list  = bq_fun_list,
  times = 2
)