38

I can create a compose operator in R:

 `%c%` = function(x,y)function(...)x(y(...)) 

To be used like this:

 > numericNull = is.null %c% numeric
 > numericNull(myVec)
 [2] TRUE FALSE

but I would like to know if there is an official set of functions to do this kind of thing and other operations such as currying in R. Largely this is to reduce the number of brackets, function keywords etc in my code.

My curry function:

> curry=function(...){
    z1=z0=substitute(...);z1[1]=call("list");
    function(...){do.call(as.character(z0[[1]]),
                          as.list(c(eval(z1),list(...))))}}
> p = curry(paste(collapse=""))
> p(letters[1:10])
[1] "abcdefghij"

This is especially nice for e.g. aggregate:

> df = data.frame(l=sample(1:3,10,rep=TRUE), t=letters[1:10])
> aggregate(df$t,df["l"],curry(paste(collapse="")) %c% toupper)
  l    x
1 1  ADG
2 2  BCH
3 3 EFIJ

Which I find much more elegant and editable than:

> aggregate(df$t, df["l"], function(x)paste(collapse="",toupper(x)))
  l    x
1 1  ADG
2 2  BCH
3 3 EFIJ

Basically I want to know - has this already been done for R?

Nathan Shively-Sanders
  • 18,329
  • 4
  • 46
  • 56
Alex Brown
  • 41,819
  • 10
  • 94
  • 108
  • note that my curry call does not currently bind curried variables from the environment where it is called, but from where the returned function is invoked. I'm working on that. – Alex Brown Feb 09 '10 at 13:52
  • 2
    If you just want a paste function with better defaults, why bother with the currying? Just define, e.g. `paste0 <- function(x, ...) paste(toupper(x), collapse="", ...)` Then you can call `aggregate(df$t, df["l"], paste0)` – Richie Cotton Feb 09 '10 at 14:05
  • 4
    because it gets more complex when you want to stack more functions. – Alex Brown Feb 09 '10 at 14:12
  • 3
    Has anyone noticed that Compose() applies the function in the [reverse order](http://tolstoy.newcastle.edu.au/R/e9/help/10/02/4529.html) of usual function composition applications? – hatmatrix Feb 11 '10 at 13:49
  • I think @AlexBrown's compose operator is better than the alternatives given in the answers, because it is infix and because it is a one-line definition (no need to install and learn about a complex library). – Metamorphic Nov 24 '16 at 20:15

6 Answers6

28

Both of these functions actually exist in the roxygen package (see the source code here) from Peter Danenberg (was originally based on Byron Ellis's solution on R-Help):

Curry <- function(FUN,...) {
  .orig = list(...);
  function(...) do.call(FUN,c(.orig,list(...)))
}

Compose <- function(...) {
  fs <- list(...)
  function(...) Reduce(function(x, f) f(x),
                       fs,
                       ...)
}

Note the usage of the Reduce function, which can be very helpful when trying to do functional programming in R. See ?Reduce for more details (which also covers other functions such as Map and Filter).

And your example of Curry (slightly different in this usage):

> library(roxygen)
> p <- Curry(paste, collapse="")
> p(letters[1:10])
[1] "abcdefghij"

Here's an example to show the utility of Compose (applying three different functions to letters):

> Compose(function(x) x[length(x):1], Curry(paste, collapse=""), toupper)(letters)
[1] "ZYXWVUTSRQPONMLKJIHGFEDCBA"

And your final example would work like this:

> aggregate(df[,"t"], df["l"], Compose(Curry(paste, collapse=""), toupper))
  l    x
1 1  ABG
2 2 DEFH
3 3  CIJ

Lastly, here's a way to do the same thing with plyr (could also easily be done with by or aggregate as already shown):

> library(plyr)
> ddply(df, .(l), function(df) paste(toupper(df[,"t"]), collapse=""))
  l   V1
1 1  ABG
2 2 DEFH
3 3  CIJ
Shane
  • 98,550
  • 35
  • 224
  • 217
  • 2
    Isn't there something wrong about using a package for literate programming for functional programming? What does that say about the modularity of R libs? – piccolbo Jun 14 '11 at 23:52
  • 9
    FWIW, `Compose` and `Curry` were moved to the `functional` package awhile ago. – Ari B. Friedman Feb 09 '12 at 19:49
7

The standard place for functional programming in R is now the functional library.

From the library:

functional: Curry, Compose, and other higher-order functions

Example:

   library(functional)
   newfunc <- Curry(oldfunc,x=5)

CRAN: https://cran.r-project.org/web/packages/functional/index.html

PS: This library substitutes the ROxigen library.

elviejo79
  • 4,592
  • 2
  • 32
  • 35
2

There is a function called Curry in the roxygen package.
Found via this conversation on the R Mail Archive.

Richie Cotton
  • 118,240
  • 47
  • 247
  • 360
2

A more complex approach is required if you want the 'names' of the variables to pass through accurately.

For example, if you do plot(rnorm(1000),rnorm(1000)) then you will get nice labels on your x- and y- axes. Another example of this is data.frame

> data.frame( rnorm(5), rnorm(5), first=rpois(5,1), second=rbinom(5,1,0.5) )
    rnorm.5. rnorm.5..1 first second
1  0.1964190 -0.2949770     0      0
2  0.4750665  0.8849750     1      0
3 -0.7829424  0.4174636     2      0
4  1.6551403  1.3547863     0      1
5  1.4044107 -0.4216046     0      0

Not that the data.frame has assigned useful names to the columns.

Some implementations of Curry may not do this properly, leading to unreadable column names and plot labels. Instead, I now use something like this:

Curry <- function(FUN, ...) {
    .orig = match.call()
    .orig[[1]] <- NULL # Remove first item, which matches Curry
    .orig[[1]] <- NULL # Remove another item, which matches FUN
    function(...) {
        .inner = match.call()
        .inner[[1]] <- NULL # Remove first item, which matches Curry
        do.call(FUN, c(.orig, .inner), envir=parent.frame())
    }
}

This is quite complex, but I think it's correct. match.call will catch all args, fully remembering what expressions defined the args (this is necessary for nice labels). The problem is that it catches too many args -- not just the ... but also the FUN. It also remembers the name of the function that's being called (Curry).

Therefore, we want to delete these first two entries in .orig so that .orig really just corresponds to the ... arguments. That's why we do .orig[[1]]<-NULL twice - each time deletes an entry and shifts everything else to the left.

This completes the definition and we can now do the following to get exactly the same as above

Curry(data.frame, rnorm(5), rnorm(5) )( first=rpois(5,1) , second=rbinom(5,1,0.5) )

A final note on envir=parent.frame(). I used this to ensure that there won't be a problem if you have external variables called '.inner' or '.orig'. Now, all variables are evaluated in the place where the curry is called.

cmc
  • 973
  • 8
  • 17
Aaron McDaid
  • 26,501
  • 9
  • 66
  • 88
  • (Answerer here) I've just invented this today. I assume it has already been done? And maybe there's a flaw I don't know about?! – Aaron McDaid Sep 30 '14 at 20:28
1

in package purrr ,now there is a function partial

  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 25 '22 at 19:41
0

If you are already using the purrr package from tidyverse, then purrr::partial is a natural choice to curry functions. From the description of purrr::partial:

# Partial is designed to replace the use of anonymous functions for
# filling in function arguments. Instead of:
compact1 <- function(x) discard(x, is.null)

# we can write:
compact2 <- partial(discard, .p = is.null)
winni2k
  • 1,460
  • 16
  • 19