The vfunc package: adding two functions in R
Overview
In mathematics, given two functions
,
it is natural to define
as the function
that maps
to . 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.
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 operatorAbove, 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. The vfunc package allows us to do this.
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")
#>
#> Attaching package: 'vfunc'
#> The following object is masked from 'package:stats':
#>
#> Gamma
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.88889Further, functions may be combined arithmetically:
(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.66667or compositionally:
(f(g) + g(f))(1:10)
#> [1] Inf 0.666666667 0.125000000 0.044444444 0.020833333 0.011428571
#> [7] 0.006944444 0.004535147 0.003125000 0.002244669The 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 , , . Given this, we wish to calculate
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.411975Note 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.411975See 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
(such expressions arise in the study of dynamical systems). Note that functions and are to be evaluated with two distinct sets of arguments at different levels of nesting, namely at the inner level and 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.04918The 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.04918which 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.
The package includes functions such as Sin() which is a vf
equivalent to base::sin(). This 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.545235with 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.545235and 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.545235See 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.
Further information
For more detail, see the package vignette
vignette("vfunc")