31
votes

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().

3
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
An alternative approach is to provide default values for all noncommon arguments. In general, trying to send the ellipsis args to more than one place leads to sadness.Carl Witthoft
I believe a more clean and robust way would be to avoid ... either for all or for all but one of the inner functions and use do.call; foo = function(x, y, sum_args = list(NULL), grep_args = list(NULL)) list(sum = do.call("sum", c(list(x), sum_args)), grep = do.call("grep", c(list("abc"), list(y), grep_args))); foo(c(1:5, NA, 6:10), "xyzabcxyz"); foo(c(1:5, NA, 6:10), "xyzabcxyz", sum_args = list(na.rm = TRUE), grep_args = list(value = TRUE))alexis_laz
@alexis_laz yeah, what I said :-) . Nicely detailed.Carl Witthoft
For primitive functions, you can retrieve the arguments using, for example, formals(args(sum)).Richie Cotton

3 Answers

32
votes

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
15
votes
  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).

0
votes

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"