10

Is it possible to chain functions in R?

Sample data:

m <- matrix(c(1:10, 11:20), nrow = 10, ncol = 2)

For example, I would like to replace the following statements below:

step1 <- mean(m)
step2 <- sum(step1)
res <- step2

Or,

res <- sum(mean(m))

With something like this :

res <- m@mean()@sum()

In some cases, that would clarify my code considerably.

EDIT1 This is a dummy example. I randomly picked 'sum' and 'mean'.

Ben has given a first piece of answer using %@% however, it prevents from using extra arguments within functions :

m %@% function1(arg1, arg2) %@% function2(arg1, arg2)

How can I work around that ?

EDIT2 Adding an example

require(xts)
require(PerformanceAnalytics)
xts.ts <- xts(rnorm(231),as.Date(13514:13744,origin="1970-01-01"))
plot(na.omit(lag( rollapply(xts.ts, width=rolling.per-1, FUN= function(x){sqrt(var(x))*sqrt(252)}), k=1)), main = "Dummy Example")

This example seems to work fine with Charles solution :

`%@%` <- function(x, f) eval.parent(as.call(append(as.list(substitute(f)), list(x), 1)))
xts.ts %@% rollapply( width = rolling.per-1, FUN= function(x) x%@%var%@%sqrt * sqrt(252) ) %@% lag( k=1) %@% na.omit %@% plot(main = "Dummy Example")

Less important to my case, but woth mentioning, the following statment fails with Charles's solution :

 xts.ts %@% names <- 'ts name' 
Sam
  • 565
  • 6
  • 23
  • 2
    What's wrong with `res <- sum(mean(m))`? – Richie Cotton Jul 04 '12 at 14:21
  • Nothing, but it doesn't make to much sense to take the `sum` of a length 1 vector (which is what is returned by `mean` on a matrix). – Henrik Jul 04 '12 at 14:27
  • Although it will be going away "soon", there is still a `mean.data.frame` function that returns a vector. – IRTFM Jul 04 '12 at 14:38
  • 1
    With a lot of function and arguments, I found it heavy where IMO method chaining may improves readability and reduces the amount of source code. But you're right, nothing wrong with res <- sum(mean(m)) – Sam Jul 04 '12 at 16:37
  • FYI this style of programming is called point-free (or sometimes pointless!). There are a few notes on it at https://github.com/hadley/devtools/wiki/Function-operators#function-composition – hadley Jul 26 '13 at 15:07

4 Answers4

12

Try the functional package:

library(functional)
squared <- function(x)x*x
Compose(sum, squared)(m)
## [1] 44100
squared(sum(m))
## [1] 44100

EDIT:

Regarding the question in the comments of another response about arguments here is an example of composing with arguments. Curry is also from the functional package:

addn <- function(n, x) x + n
Compose(Curry(addn, 1), squared)(10)
## [1] 121
squared(addn(1, 10))
## [1] 121

EDIT 2:

Regarding question about debugging, debug works if the function is curried. If its not already curried then wrap it in Curry :

# this works since addn is curried
debug(addn)
Compose(Curry(addn, 1), squared)(10)

# to debug squared put it in a Curry -- so this works:
debug(squared)
Compose(Curry(addn, 1), Curry(squared))(10)
G. Grothendieck
  • 254,981
  • 17
  • 203
  • 341
  • I thought the gsubfn-package could do something like that, at least the postfix syntax of env@fn? – IRTFM Jul 04 '12 at 19:37
  • @DWin, Good point. In gsubfn we could do this: `Compose(fn$identity(~ addn(1, x)), squared)(10)` . Of course even without gsubfn we could do this: `Compose(function(x) addn(1, x), squared)(10)` – G. Grothendieck Jul 04 '12 at 19:44
  • @G.Grothendieck, it works but I do not think it gives you the usual advantages of fluent interface (http://en.wikipedia.org/wiki/Fluent_interface) as some other object oriented languages would. Furthermore, I believe it does not make easy to debbug part of a statment. – Sam Jul 05 '12 at 09:57
  • @Sam, `debug` works if function is in a `Curry`. See `EDIT 2` . – G. Grothendieck Jul 05 '12 at 14:52
9

Sort of, but I think it's un-idiomatic and maybe fragile/not a good idea. (This is implied, I think, by @RichieCotton's comment above.)

From http://cran.r-project.org/doc/manuals/R-lang.html :

10.3.4 Special operators

R allows user-defined infix operators. These have the form of a string of characters delimited by the ‘%’ character. The string can contain any printable character except ‘%’. The escape sequences for strings do not apply here.

Note that the following operators are predefined

 %% %*% %/% %in% %o% %x%
"%@%" <- function(x,f) {
    f(x)
}

sqr <- function(x) x^2
x <- 1:4

x %@% mean  ## 2.5
x %@% mean %@% sqr  ## 6.25
x %@% (mean %@% sqr)  ## fails

Given m as defined above -- maybe what you had in mind?

 m %@% colMeans %@% sum  ## 21

Notes:

  • your example is a bit funny, because mean(x) always returns a scalar (i.e. a length-1 vector), so sum(mean(x)) is always going to be the same as mean(x)
  • the infix operators have to be surrounded by %, so you can't have anything as compact as a single symbol (and %% is taken already).
  • this sort of chaining is non-associative, which worries me -- it seems that the examples above work, so R is (apparently) evaluating left-to-right, but I don't know that that's guaranteed ...

edit: the question now asks how additional arguments can be incorporated. I don't think the syntax suggested (x %@% fun1(arg1) %@% fun2(arg2)) will work without some serious magic. This is the closest I can get at the moment -- creating a wrapper function that creates a modified version of the original function.

F <- function(f,...) {
    function(x) {
        f(x,...)
    }
}

Testing:

pow <- function(x,b=2) { x^b }
sqr <- function(x) x^2
x <- 1:4

x %@% F(mean,na.rm=TRUE)  ## 2.5
x %@% F(mean,na.rm=TRUE) %@% F(pow,3)  ## 16.25

(Note that I have used F as a function here, which may be dicey in some situations because it overwrites the F==FALSE shortcut)

Ben Bolker
  • 211,554
  • 25
  • 370
  • 453
  • Seems to be a work around but it may just do the trick fine! How would you suggest to modify the %@% operator so I can add arguments ? For instance : m %@% function1(arg1, arg2) % function2(arg1, arg2) – Sam Jul 04 '12 at 17:04
  • 1
    What is with `"%@%" <- function(f,...) f(...)`? With this all arguments should be passed to `f`, even named arguments. (note: untested) – Henrik Jul 04 '12 at 18:08
  • @Sam, can you give me an specific example of what you want to do and what results it should produce? Your syntax won't work exactly as written because `function1(arg1,arg2)` is not in general a function. I think @Henrik's idea may not work, because we have to be careful to keep the arguments in the right order ... `f` should be the *second* argument in order to get the operation in the correct left-to-right order ... – Ben Bolker Jul 04 '12 at 18:19
  • @Henrik I was not able to use your function, there is something I do not understand. – Sam Jul 05 '12 at 09:16
  • @Ben : the argument order may be an issue. I will edit my question to add an example. – Sam Jul 05 '12 at 09:17
5

I would use the magrittr package. It has a "pipe" operator that takes the result of one function and passes it in as an argument to the next:

m <- matrix(c(1:10, 11:20), nrow = 10, ncol = 2)

m %>% mean %>% sum

Ceci n'est pas un pipe!

sdgfsdh
  • 33,689
  • 26
  • 132
  • 245
4

In a similar vein to Ben's answer, but allowing arguments:

`%@%` <- function(x, f) eval.parent(as.call(append(as.list(substitute(f)), list(x), 1)))

x %@% mean %@% sqr # => 6.25
c(1, 2, NA, 3, 4) %@% mean(na.rm=T) %@% sqr # => 6.25
m %@% colMeans() %@% sum() # => 21
Charles
  • 4,389
  • 2
  • 16
  • 13
  • To get the last one to work you'd need to use either `names<-` or `setNames`, eg `xts.ts %@% setNames('ts name')`, as `<-` has special handling for lhs function call that won't work here. – Charles Jul 08 '12 at 23:55
  • I hadn't seen this before. It's super-clever. I'd be very afraid to use it in production code because of the chances that it would be fragile ... – Ben Bolker Feb 21 '13 at 20:19
  • I wouldn't want to use this in code - production or otherwise! Regardless of whether this is a good idea or not, what I like about R is that this kind of thing is possible. I can see how it would look more familiar to someone who is used to the common OO `object.verb()` instead of R's `verb(object)`. – Charles Feb 21 '13 at 23:13