function.compose: Concatenate Two Functions

Description Usage Arguments Examples

Description

Try to find a small and efficient representation of the concatenation of to functions f and g such that the resulting function has the form h(...)=g(...,f(...),...) (where ... stands for an arbitrary numer of arguments, not the R-style dots).

More precisely: The input are two functions f and g and a "bridge argument" f2g. Both functions may have an arbirary argument list, say f(x,y,z=7,c) and g(a=3,b=4,c). The "bridge argument" can be any parameter of g, let's say b. The result of function.compose(f, g, f2g="b") will then be a function h(a=3,c,x,y,z=7) which is equivalent to g(a, f(x, y, z, c), c).

Since we also canonicalize all functions (f, g, and h) involved, this means also that we attempt to substitute all potentially substitutable variables and simplify the inner expressions as much as possible.

This is particularly useful when iteratively composing functions and wanting to retain their readability and making the result as fast to execute as possible. However, the behavior may be undefined if the parameters of the function can be resolved as constant values in the current environment or if functions with otherwise odd parameters are successfively composed.

If the result of f is used exactly once in g, we substitute the body of f into g to directly replace the bridge parameter. If it is used more than once, we first shovel its result into a variable, namely the bridge parameter.

As example, assume you have set k <- 23 and now want to compose the functions f<-function(x) { (k*x) + 7 } and g<-function(x) { (k*k-x*x) / (x - sin(k)) } to a function h. You can do that by writing h<-function(x) g(f(x)). Of course, if you later try to inspect h and just write h, you will see exactly this, function(x) g(f(x)). This leads to two issues: First, if you do not know f and g, the output is meaningless and opaque, you cannot interpret it. Second, evaluating h is unnecessarily slow: It performs two inner function calls and needs to evaluate a variable k at several locations, although the value of k should be fixed to 23. Matter of fact, also k*k and sin(k) are constants which could be known.

The goal of function.compose is to resolve these two issues. If you do h<-function.compose(f, g) instead of h<-function(x) g(f(x)), a new function composed of both the bodies of f and g is created. Furthermore, as many of the variables and expressions in the body which can be resolved as possible are replaced by their constant result. Printing the result of h<-function.compose(f, g) would yield (something like) function (x) { x <- (23 * x) + 7; (529 - x * x)/(x - -0.846220404175171) }.

Additionally, we replace identical sub-expressions in the composed function by the same object. This may be advantegous for a slightly faster execution due to being more cache friendly.

Usage

1
function.compose(f, g, f2g = "x")

Arguments

f

the inner function

g

the outer function

f2g

the argument of g to be replaced with the return value of f

Examples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
f<-function(x,y,z=7,c) { x+23*y-z+2+c }
g<-function(a=3,b=4,c) { a*b - b*c }
function.compose(f, g, f2g="b")
# function (a = 3, c, x, y, z = 7)
# {
#   b <- x + 23 * y - z + 2 + c
#   a * b - b * c
# }
function.compose(sin, cos, f2g="x")
# function (x)
# cos(x = sin(x = x))
k <- 23
f2 <- function(x) { k*5 + x }
g2 <- function(x) { x/(k - sin(k)) }
function.compose(f2, g2)
# function (x)
# (115 + x)/23.8462204041752
k<-23
f<-function(x) { (k*x) + 7 }
g<-function(x) { (k*k-x*x) / (x - sin(k)) }
h.plain<-function(x) g(f(x))
h.plain
# function(x) g(f(x))
h.composed<-function.compose(f, g)
h.composed
# function (x)
# {
#   x <- (23 * x) + 7
#   (529 - x * x)/(x - -0.846220404175171)
# }
i<-45
j<-33
k<-23
f <- function(x) { (x*(x-i)) - x/sinh(k*cos(j-atan(k+j))) }
g <- function(x) { abs(x)^(abs(1/(3-i))) + (j - k*exp(-i)) / ((i*j) * x) }
h.1.plain <- function(x) g(f(x))
h.1.plain
# function(x) g(f(x))
h.1.composed <- function.compose(f, g)
h.1.composed
# function (x)
# {
#     x <- (x * (x - 45)) - x/4818399372.40284
#     abs(x)^0.0238095238095238 + 33/(1485 * x)
# }
h.2.plain <- function(x) g(f(g(f(x))))
h.2.plain
# function(x) g(f(g(f(x))))
h.2.composed <- function.compose(function.compose(function.compose(f, g), f), g)
h.2.composed
# function (x)
# {
#     x <- {
#         x <- {
#             x <- (x * (x - 45)) - x/4818399372.40284
#             abs(x)^0.0238095238095238 + 33/(1485 * x)
#         }
#         (x * (x - 45)) - x/4818399372.40284
#     }
#     abs(x)^0.0238095238095238 + 33/(1485 * x)
# }
x <- runif(1000)
library(microbenchmark)
microbenchmark(h.1.plain(x), h.1.composed(x), h.2.plain(x), h.2.composed(x))
# Unit: microseconds
#             expr     min       lq      mean   median       uq     max neval
#     h.1.plain(x)  78.841  79.4880  83.05224  79.9775  85.8485 119.824   100
#  h.1.composed(x)  75.890  76.4675  93.23504  76.8385  78.9615 896.681   100
#     h.2.plain(x) 153.793 154.8100 166.31210 155.5855 164.3685 743.360   100
#  h.2.composed(x) 149.035 149.4870 155.25070 149.8895 154.0960 213.395   100

thomasWeise/functionComposeR documentation built on May 28, 2019, 4:03 p.m.