33

Using the following function foo() as a simple example, I'd like to distribute the values given in ... two different functions, if possible.

foo <- function(x, y, ...) {
    list(sum = sum(x, ...), grep = grep("abc", y, ...))
}

In the following example, I would like na.rm to be passed to sum(), and value to be passed to grep(). But I get an error for an unused argument in grep().

X <- c(1:5, NA, 6:10)
Y <- "xyzabcxyz"
foo(X, Y, na.rm = TRUE, value = TRUE)
# Error in grep("abc", y, ...) : unused argument (na.rm = TRUE)

It seems like the arguments were sent to grep() first. Is that correct? I would think R would see and evaluate sum() first, and return an error for that case.

Furthermore, when trying to split up the arguments in ..., I ran into trouble. sum()'s formal arguments are NULL because it is a .Primitive, and therefore I cannot use

names(formals(sum)) %in% names(list(...))

I also don't want to assume that the leftover arguments from

names(formals(grep)) %in% names(list(...))

are to automatically be passed to sum().

How can I safely and efficiently distribute ... arguments to multiple functions so that no unnecessary evaluations are made?

In the long-run, I'd like to be able to apply this to functions with a long list of ... arguments, similar to those of download.file() and scan().

Rich Scriven
  • 97,041
  • 11
  • 181
  • 245
  • 1
    The exact same `...` values are sent to both function. `sum(X, na.rm = TRUE, value = TRUE)` does not return an error (note that `sum(1, na.rm = TRUE, value = TRUE)==2`), but `grep("abc",Y, na.rm = TRUE, value = TRUE)` does.R will not change the `...` based on what's actually used by a function. If you want to split up the parameters, you will have to do it by yourself. – MrFlick Aug 19 '14 at 05:53
  • 2
    For primitive functions, you can retrieve the arguments using, for example, `formals(args(sum))`. – Richie Cotton Aug 27 '14 at 09:44
  • @RichieCotton - That is really useful. Thank you. Surprised that I've never seen that used before. – Rich Scriven Aug 28 '14 at 00:37

5 Answers5

33

Separate Lists If you really want to pass different sets of parameters to different functions then it's probably cleaner to specify separate lists:

foo <- function(x, y, sum = list(), grep = list()) {
 list(sum = do.call("sum", c(x, sum)), grep = do.call("grep", c("abc", y, grep)))
}

# test

X <- c(1:5, NA, 6:10)
Y <- "xyzabcxyz"
foo(X, Y, sum = list(na.rm = TRUE), grep = list(value = TRUE))

## $sum
## [1] 55
## 
## $grep
## [1] "xyzabcxyz"

Hybrid list / ... An alternative is that we could use ... for one of these and then specify the other as a list, particularly in the case that one of them is frequently used and the other is infrequently used. The frequently used one would be passed via ... and the infrequently used via a list. e.g.

foo <- function(x, y, sum = list(), ...) {
 list(sum = do.call("sum", c(x, sum)), grep = grep("abc", y, ...))
}

foo(X, Y, sum = list(na.rm = TRUE), value = TRUE)

Here are a couple of examples of the hybrid approach from R itself:

i) The mapply function takes that approach using both ... and a MoreArgs list:

> args(mapply)
function (FUN, ..., MoreArgs = NULL, SIMPLIFY = TRUE, USE.NAMES = TRUE) 
NULL

ii) nls also takes this approach using both ... and the control list:

> args(nls)
function (formula, data = parent.frame(), start, control = nls.control(), 
    algorithm = c("default", "plinear", "port"), trace = FALSE, 
    subset, weights, na.action, model = FALSE, lower = -Inf, 
    upper = Inf, ...) 
NULL
G. Grothendieck
  • 254,981
  • 17
  • 203
  • 341
17
  1. Why does grep error before sum?

    See that sum is a lot more accommodating with its arguments:

    X <- c(1:5, NA, 6:10)
    sum(X, na.rm = TRUE, value = TRUE)
    ## [1] 56
    

    It doesn't failed because it doesn't care about other named arguments, so the value = TRUE simplifies to just TRUE which sums to 1. Incidentally:

    sum(X, na.rm = TRUE)
    ## [1] 55
    
  2. How to split ... to different functions?

    One method (that is very prone to error) is to look for the args for the target functions. For instance:

    foo <- function(x, y, ...){
        argnames <- names(list(...))
        sumargs <- intersect(argnames, names(as.list(args(sum))))
        grepargs <- intersect(argnames, names(as.list(args(grep))))
        list(sum = do.call(sum, c(list(x), list(...)[sumargs])),
             grep = do.call(grep, c(list("abc", y), list(...)[grepargs])))
    }
    

    This is prone to error anytime the arguments a function uses are not properly reported by args, such as S3 objects. As an example:

    names(as.list(args(plot)))
    ## [1] "x"   "y"   "..." ""   
    names(as.list(args(plot.default)))
    ##  [1] "x"           "y"           "type"        "xlim"        "ylim"       
    ##  [6] "log"         "main"        "sub"         "xlab"        "ylab"       
    ## [11] "ann"         "axes"        "frame.plot"  "panel.first" "panel.last" 
    ## [16] "asp"         "..."         ""           
    

    In this case, you could substitute the appropriate S3 function. Because of this, I don't have a generalized solution for this (though I don't know that it does or does not exist).

r2evans
  • 141,215
  • 6
  • 77
  • 149
2

TL;DR

Consider using the "trick" implemented in plot.default and elsewhere. It is tried and tested.

foo <- function(x, y, ...) {
    localSum <- function(..., value) sum(...)
    localGrep <- function(..., na.rm) grep(...)
    list(sum = localSum(x, ...), grep = localGrep("abc", y, ...))
}

X <- c(1:5, NA, 6:10)
Y <- "xyzabcxyz"
foo(X, Y, na.rm = TRUE, value = TRUE)
## $sum
## [1] 55
## 
## $grep
## [1] "xyzabcxyz"
## 

I am a bit shocked that the "trick" used by plot.default hasn't come up here yet. (Maybe it is mentioned elsewhere?)

Recall that plot.default must process the optional arguments of the internal plot.xy (for points and lines) as well as those of plot.window, box, axis, and title. These are all handled through one ... argument.

Importantly, when we pass (say) lwd = 6 to plot.default to widen lines in the plot region, plot.default must ensure that this argument is not seen by box and axis, which use lwd to set the width of the plot region border and axis lines. Hence:

> plot.default(0:1, 0:1, type = "l", lwd = 6)

enter image description here

This feature of plot.default is implemented using "local" versions of the subfunctions plot.window, box, axis, and title. The local versions are just wrappers designed to filter out arguments in ... that the subfunctions cannot be allowed to see (in this case col, bg, pch, cex, lty, and lwd).

> plot.default
function (x, y = NULL, type = "p", xlim = NULL, ylim = NULL, 
    log = "", main = NULL, sub = NULL, xlab = NULL, ylab = NULL, 
    ann = par("ann"), axes = TRUE, frame.plot = axes, panel.first = NULL, 
    panel.last = NULL, asp = NA, xgap.axis = NA, ygap.axis = NA, 
    ...) 
{
    localAxis <- function(..., col, bg, pch, cex, lty, lwd) Axis(...)
    localBox <- function(..., col, bg, pch, cex, lty, lwd) box(...)
    localWindow <- function(..., col, bg, pch, cex, lty, lwd) plot.window(...)
    localTitle <- function(..., col, bg, pch, cex, lty, lwd) title(...)
    xlabel <- if (!missing(x)) 
        deparse1(substitute(x))
    ylabel <- if (!missing(y)) 
        deparse1(substitute(y))
    xy <- xy.coords(x, y, xlabel, ylabel, log)
    xlab <- if (is.null(xlab)) 
        xy$xlab
    else xlab
    ylab <- if (is.null(ylab)) 
        xy$ylab
    else ylab
    xlim <- if (is.null(xlim)) 
        range(xy$x[is.finite(xy$x)])
    else xlim
    ylim <- if (is.null(ylim)) 
        range(xy$y[is.finite(xy$y)])
    else ylim
    dev.hold()
    on.exit(dev.flush())
    plot.new()
    localWindow(xlim, ylim, log, asp, ...)
    panel.first
    plot.xy(xy, type, ...)
    panel.last
    if (axes) {
        localAxis(if (is.null(y)) 
            xy$x
        else x, side = 1, gap.axis = xgap.axis, ...)
        localAxis(if (is.null(y)) 
            x
        else y, side = 2, gap.axis = ygap.axis, ...)
    }
    if (frame.plot) 
        localBox(...)
    if (ann) 
        localTitle(main = main, sub = sub, xlab = xlab, ylab = ylab, 
            ...)
    invisible()
}
<bytecode: 0x10b813010>
<environment: namespace:graphics>

Let's take localTitle as an example:

localTitle <- function(..., col, bg, pch, cex, lty, lwd) title(...)

plot.default passes all of the arguments in ... to localTitle when it calls

localTitle(main = main, sub = sub, xlab = xlab, ylab = ylab, ...)

but title only ever sees those arguments not named col, bg, pch, cex, lty, or lwd, since those are included as formal arguments of localTitle.

Notably, this approach to processing ... has minimal overhead as it takes full advantage of lazy evaluation. None of the expressions in ... are evaluated until they are used by the subfunctions.

Mikael Jagan
  • 9,012
  • 2
  • 17
  • 48
1

You can only pass the ... argument to another function, if that other function includes all named arguments that you pass to ... or if it has a ... argument itself. So for sum, this is no problem (args(sum) returns function (..., na.rm = FALSE)). On the other hand grep has neither na.rm nor ... as an argument.

args(grep)
# function (pattern, x, ignore.case = FALSE, perl = FALSE, value = FALSE, 
#     fixed = FALSE, useBytes = FALSE, invert = FALSE) 

This does not include ... and also does not include a named argument na.rm either. A simple solution is to just define your own function mygrep as follows:

mygrep <- function (pattern, x, ignore.case = FALSE, perl = FALSE, value = FALSE, 
                    fixed = FALSE, useBytes = FALSE, invert = FALSE, ...)
  grep(pattern, x, ignore.case, perl, value, fixed, useBytes, invert)

Then it seems to work:

foo <- function(x, y, ...){
  list(sum = sum(x, ...), grep = mygrep("abc", y, ...))
}
X <- c(1:5, NA, 6:10)
Y <- "xyzabcxyz"
foo(X, Y, na.rm = TRUE, value = TRUE)

# $sum
# [1] 56
# 
# $grep
# [1] "xyzabcxyz"
shadow
  • 21,823
  • 4
  • 63
  • 77
  • 1
    The first sentence isn't right - you can pass `...` even if the other function doesn't take `...`. For example: `add <- function(x, y) x + y ; f <- function(...) add(...) ; f(3,4)` – wch Aug 19 '14 at 16:09
  • @wch: Thanks for pointing that out. Changed the first sentence accordingly. – shadow Aug 19 '14 at 16:14
  • First paragraph is still misleading – Dason Aug 19 '14 at 16:19
0

This answer does not directly the original question but could be helpful to others who experience a similar problem with their own functions (as opposed to existing functions like sum and grep).

@shadow's answer contains an insight that points to a very simple solution in such cases: just make sure your nested functions have ... as an argument and you won't get the unused argument error.

For example:

nested1 <- function(x, a) {
 x + a
}

nested2 <- function(x, b) {
 x - b
}

f <- function(x, ...) {
 if (x >= 0) {
  nested1(x, ...)
 } else {
  nested2(x, ...)
 }
}

If we call f(x = 2, a = 3, b = 4) we get an error: Error in nested1(x, ...) : unused argument (b = 4).

But just add a ... to the formals of nested1 and nested2 and run again:

nested1 <- function(x, a, ...) {
 x + a
}

nested2 <- function(x, b, ...) {
 x - b
}

Now, f(x = 2, a = 3, b = 4) yields the desired result: 5. Problem solved.

Thomas Farrar
  • 253
  • 1
  • 7