Function factorization with the vfunc package
Author: Robin K. S. Hankin

# Function factorization with the `vfunc` package

#### Robin K. S. Hankin

To cite the `vfunc` package in publications please use R Core Team (2024).

In mathematics, given two functions \(f,g\colon\mathbb{R}\longrightarrow\mathbb{R}\), it is natural to define \(f+g\) as the function that maps \(x\in\mathbb{R}\) to \(f(x) + g(x)\). However, in base R, objects of class `function` do not have arithmetic methods defined, so idiom such as `f + g` returns an error, even though it has a perfectly reasonable expectation. The `vfunc package` offers this functionality. Other similar features are provided, which lead to compact and readable idiom. A wide class of coding bugs is eliminated.

# Introduction

Consider the following R session:

    f <- function(x){x^2}
    g <- function(x){1/(1-x)}
    f + g

    ## Error in f + g: non-numeric argument to binary operator

Above, there is a reasonably clear expectation for `f + g`: it should give a function that returns the sum of `f()` and `g()`; something like `function(x){f(x) + g(x)}`. However, it returns an error because `f` and `g` are objects of `S4` class `function`, which do not have an addition method. Further, it is not possible to define `Arith` group `S4` methods [in this case, overloading addition] so that this idiom operates as desired. This is because the `function` class is sealed in `S4`: the definition of new methods for it is prohibited. Here I present the `vfunc` R package that furnishes appropriate idiom. The package defines a new `S4` class `vf` (“virtual function”) which inherits from `function`, but for which new methods can be defined. This device furnishes some ways to apply `Arith` methods for functions.

# The package in use

The package is designed so that objects of class `vf` operate as functions but are subject to arithmetic operations, which are executed transparently. For example:

    library("vfunc")
    f <- as.vf(f)
    g <- as.vf(g)
    (f + g)(1:10)

    ##  [1]      Inf  3.00000  8.50000 15.66667 24.75000 35.80000 48.83333 63.85714
    ##  [9] 80.87500 99.88889

Above, we coerce `f` and `g` to objects of `S4` class `vf` [for “virtual function”]. Such objects have `Arith` methods defined and may be combined arithmetically; for example addition is dispatched to

    function(e1, e2){as.vf(function(...){e1(...) + e2(...)})}

The `vf` class has a single `.Data` slot of type `function` which means that objects of this class inherit much of the behaviour of base class `function`; above, we see that `e1` and `e2` may be executed with their argument list directly. In practice this means that `f+g` behaves as intended, and suggests other ways in which it can be used:

    (f + 4*g - f*g)(1:10)

    ##  [1]       NaN   4.00000  11.50000  20.00000  30.25000  42.40000  56.50000
    ##  [8]  72.57143  90.62500 110.66667

The advantages of such idiom fall in to two main categories. Firstly, code can become considerably more compact; and secondly one can guard against a wide class of hard-to-find bugs. Now consider `f()` and `g()` to be trivariate functions, each taking three arguments, say,

    f <- function(x,y,z){x + x*y - x/z}
    g <- function(x,y,z){x^2 - z}

and \(x=1.2\), \(y=1.7\), \(z=4.3\). Given this, we wish to calculate

\[(f(x,y,z) + g(x,y,z))(f(x,y,z) + 4 - 2f(x,y,z)g(x,y,z)).\]

How would one code up such an expression in R? The standard way would be

     x <- 1.2
     y <- 1.7
     z <- 4.3
    (f(x,y,z) + g(x,y,z))*(f(x,y,z) + 4 - 2*f(x,y,z)*g(x,y,z))

    ## [1] 2.411975

Note the repeated specification of argument list `(x,y,z)`, repeated here five times. Now use the `vfunc` package:

    f <- as.vf(f)
    g <- as.vf(g)
    ((f + g)*(f + 4 - 2*f*g))(x,y,z)

    ## [1] 2.411975

See how the package allows one to ‘’factorize’’ the argument list so it appears once, leading to more compact code. It is also arguably less error-prone, as the following example illustrates. Consider

\[ f(x+z,y+z,f(x,x,y)-g(x,x,y)) + g(x+z, y+z,f(x,x,y)-g(x,x,y)) \]

(such expressions arise in the study of dynamical systems). Note that functions \(f\) and \(g\) are to be evaluated with two distinct sets of arguments at different levels of nesting, namely \((x,x,y)\) at the inner level and \((x+z,y+z,f(x,x,y)-g(x,x,y)\) at the outer. Standard R idiom would be

    f(x + z, y + z, f(x, x, y) - g(x, x, y)) + g(x + z, y + z, f(x, x, y) - g(x, x, y))

    ## [1] 64.04918

The author can attest that finding bugs in such expressions can be difficult [it is easy to mistype `(x,x,y)` in one of its occurrences, yet difficult to detect the error]. However, `vfunc` idiom would be

    (f + g)(x + z, y + z, (f - g)(x, x, y))

    ## [1] 64.04918

which is certainly shorter, arguably neater and at least the author finds such constructions considerably less error-prone. In this form, one can be sure that both `f()` and `g()` are called with identical arguments at each of the two levels in the expression, as the arguments appear only once.

## Overloading

Looking again at the method for `vf` addition, viz

    function(e1, e2){as.vf(function(...){e1(...) + e2(...)})}

we see the `+` operator is used to sum the return values of `e1()` and `e2()`. There is no reason that this operator cannot itself be overloaded, and the `vfunc` package works transparently if this is the case, with either `S3` or `S4`. Taking the `onion` package (Hankin 2006) as an example:

    library("onion")
    options("show_onions_compactly" = TRUE)
    f <- as.vf(function(x,y){x + x*y})
    g <- as.vf(function(x,y){x^2 + y})
    (f + g - f*g)(1 + Hj,Hk)

    ##         Re
    ## 4+2i+2j-1k

## Primitive functions

The R language includes a number of primitive functions as `S4` Math generics, including the trig functions such as `sin()`, and a few others such as the cumulative sum `cumsum()`. These functions are quite deep-seated and cannot easily be modified to work with objects of class `vf`. The package defines capitalized versions of primitive functions to operate with other objects of class `vf`. Taking `sin()` as an example we have

    vfunc::Sin

    ## An object of class "vf"
    ## function (x)
    ## {
    ##     sin(x)
    ## }
    ## <bytecode: 0x56005577d918>
    ## <environment: namespace:vfunc>

Then we may, for example, combine trig functions with user-defined functions:

    fun <- as.vf(function(x){x^2 + 2})
    (fun(Sin) + Sin(fun) - 3*Sin*fun)(0.32)

    ## [1] 0.9769132

Above, we see package idiom being used to evaluate \(\sin^2(0.32) + 3 + \sin(0.32^2+2) - 3\cdot\sin 0.32\cdot(0.32^2+2)\). In base R:

    fun(sin(0.32)) + sin(fun(0.32)) - 3*sin(0.32)*fun(0.32)

    ## [1] 0.9769132

This construction allows one to define composite functions such as

    j <- as.vf(function(x,y){Cos(x) + Sin(x-y)})
    k <- as.vf(function(x,y){Tan(x) + Log(x+y)})
    l <- as.vf(function(x,y){Sin(x/2) + x^2   })

(note that functions `j()`, `k()` and `l()` are bivariate). Then compare

    (j + k + l)(Sin + Log, Cos + Exp)(Sin + Tan)(0.4)

    ## [1] 2.545235

with the one-stage idiom which reads:

    j(sin(sin(0.4) + tan(0.4)) + log(sin(0.4) + tan(0.4)), cos(sin(0.4) + tan(0.4)) +
    exp(sin(0.4) + tan(0.4))) + k(sin(sin(0.4) + tan(0.4)) + log(sin(0.4) + tan(0.4)),
    cos(sin(0.4) + tan(0.4)) + exp(sin(0.4) + tan(0.4)))+ l(sin(sin(0.4) + tan(0.4)) +
    log(sin(0.4) + tan(0.4)), cos(sin(0.4) + tan(0.4)) + exp(sin(0.4) + tan(0.4)))

    ## [1] 2.545235

and the multi-stage idiom:

    A <- function(x,y){j(x,y) + k(x,y) + l(x,y)}
    B <- function(x){sin(x) + log(x)}
    C <- function(x){cos(x) + exp(x)}
    D <- function(x){sin(x) + tan(x)}
    x <- 0.4
    A(B(D(x)), C(D(x)))

    ## [1] 2.545235

See how the one-stage idiom is very long, and the multi-stage idiom is opaque [and nevertheless has repeated instances of `(x,y)` and `x`].

# Conclusions

The `vfunc` package allows functions to be ‘’factorized’’, that is, `f(x) + g(x)` to be re-written `(f + g)(x)`. This allows for concise idiom and eliminates a certain class of coding errors. The package also allows for recursive application of such ideas.

## References

Hankin, R. K. S. 2006. “Normed Division Algebras with R: Introducing the Onion Package.” R News 6 (2): 49–52.
R Core Team. 2024. R: A Language and Environment for Statistical Computing. Vienna, Austria: R Foundation for Statistical Computing. https://www.R-project.org/.