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.