Search code examples
rfunctionparametersellipsis

Build function before passing it to other functions


My goal is to take additional arguments passed through the ellipsis ... (see ?dots for more info) and build a new generic function with the parameters already set and pass this to another function.

For example, given the two functions:

foo <- function(v, FUN, ...) {
    ## code here to build NEWFUN
    SomeFun(v, NEWFUN)
}

bar <- function(v, FUN) {
    SomeFun(v, FUN)
}

I would like to be able to do this in foo:

bar(x, FUN = \(x) paste(x, collapse = ", "))

By calling foo(x, paste, collapse = ", ").

My Attempt

We start with a simple function takes a base R function (here paste) and applys it to a vector. Note, I'm trying to make this as simple as possible, so I have removed sanity checks. Also, I wrote this to be demonstrated only with the base R function paste.

FunAssign <- function(f, x) f(x)

And here is my naive attempt:

foo <- function(v, FUN, ...) {
    FUN <- \(x) FUN(x, ...)
    FunAssign(FUN, v)
}

And calling it, we get the error:

foo(letters[1:5], paste, collapse = ", ")
#> Error in FUN(x, ...) : unused argument (collapse = ", ")

As explained above, the desired output when calling foo(letters[1:5], paste, collapse = ", ") can be emulated by calling bar like so:

bar <- function(v, FUN) FunAssign(FUN, v)

bar(letters[1:5], FUN = \(x) paste(x, collapse = ", "))
#> [1] "a, b, c, d, e"

I thought my attempt with foo would work because if we do something like the below it appears that we are on the right track:

baz <- function(FUN, ...) \(x) FUN(x, ...)

baz(paste, collapse = ", ")(letters[1:5])
#> [1] "a, b, c, d, e"

I have found several resources that almost get at what I'm after, but nothing that quite hits the spot.


Solution

  • I think your first idea would work if you didn't overwrite the FUN variable in foo(). That is, this works for me:

    FunAssign <- function(f, x) f(x)
    
    foo <- function(v, FUN, ...) {
        force(FUN)
        FUN2 <- \(x) FUN(x, ...)
        FunAssign(FUN2, v)
    }
    
    foo(letters[1:5], paste, collapse = ", ")
    

    Besides renaming the second FUN in foo(), I added the force(FUN) command. This is not necessary in this example, but generally speaking it's a good idea to make sure arguments that are needed in a created function are evaluated. I think that automatically happens in this code (since FunAssign will use it), but I'm superstitious about things like that.

    Edited to add: the explanation for this is fairly simple.

    • The variable FUN in the function FUN2() is not evaluated until FUN2() is called.
    • When FUN2 is called within FunAssign(FUN2, v), R tries to evaluate FUN(x, ...).
    • Since FUN is not defined in that function, R looks in the parent environment, and finds FUN in the evaluation frame of foo() as an argument to the function call.
    • In your original code where you had both objects named FUN, that lookup happened after you had overwritten the argument with a different variable (the function I called FUN2).
    • When R tries to evaluate FUN(x, ...), in your code the FUN it is working with is the one that was defined right there, and R attempts a recursive call, but gives an error because that function only has one argument named x.

    The key thing here is that the body of a function is just an unevaluated expression. It doesn't get evaluated until you call the function, and that's when R tries to find all the objects it uses, including the functions it calls.