rargumentsr-s4optional-argumentsmethod-dispatch

Method dispatch with missing arguments


How can I avoid the classic Error: argument "<argname>" is missing, with no default error (see example below) when explicitly dispatching argument values to subsequent S4 methods in a given S4 method.


Example

Big picture

Now, the crucial point here is that I don't want foo() to care if any or all of the arguments that are passed along to bar() are missing.

Generic methods

setGeneric(
    name="foo",
    signature=c("x", "y"),
    def=function(x, y, ...) {
        standardGeneric("foo")
    }
)
setGeneric(
    name="bar",
    signature=c("x", "y"),
    def=function(x, y, ...) {
        standardGeneric("bar")
    }
)

Methods for bar()

setMethod(
    f="bar", 
    signature=signature(x="missing", y="missing"), 
    definition=function(x, y, ...) {
        print("Doing what I'm supposed to do when both args are missing")
        return(NULL)
    }
)
setMethod(
    f="bar", 
    signature=signature(x="ANY", y="missing"), 
    definition=function(x, y, ...) {
        message("'y' is missing, but I can give you 'x':")
        print(x)
        return(NULL)
    }
)
setMethod(
    f="bar", 
    signature=signature(x="missing", y="ANY"), 
    definition=function(x, y, ...) {
        message("'x' is missing, but I can give you 'y':")
        print(y)
        return(NULL)
    }
)
setMethod(
    f="bar", 
    signature=signature(x="ANY", y="ANY"), 
    definition=function(x, y, ...) {
        message("x:")
        print(x)
        message("y:")
        print(y)
        return(NULL)
    }
)

Method for foo()

As mentioned above, I don't want foo() to care if any or all of the arguments that are passed along to bar() are missing. It is just supposed to pass everything along to bar() in an explicit manner:

setMethod(
    f="foo", 
    signature=signature(x="ANY", y="ANY"), 
    definition=function(x, y, ...) {
        bar(x=x, y=y)    
    }
)

The method def might look good on first sight, but it will fail if either x or y are missing when calling it:

> foo(x="Hello", y="World!")
x:
[1] "Hello"
y:
[1] "World!"
NULL
> foo(x="Hello")
Error in bar(x = x, y = y) : 
  error in evaluating the argument 'y' in selecting a method for function 'bar': Error: argument "y" is missing, with no default
> foo()
Error in bar(x = x, y = y) : 
  error in evaluating the argument 'x' in selecting a method for function 'bar': Error: argument "x" is missing, with no default

Workaround

This is the only workaround that I could come up with so far:

setMethod(
    f="foo", 
    signature=signature(x="ANY", y="ANY"), 
    definition=function(x, y, ...) {
        if (missing(x) && missing(y)) {
            bar()    
        } else if (missing(x)) {
            bar(y=y)
        } else if (missing(y)) {
            bar(x=x)
        } else {
            bar(x=x, y=y)
        }
    }
)

> foo(x="Hello", y="World!")
x:
[1] "Hello"
y:
[1] "World!"
NULL
> foo(x="Hello")
'y' is missing, but I can give you 'x':
[1] "Hello"
NULL
> foo(y="World!")
'x' is missing, but I can give you 'y':
[1] "World!"
NULL
> foo()
[1] "Doing what I'm supposed to do when both args are missing"
NULL

It works, but I don't really like it because of all the if ... else statements. The whole "if-else logic" already went in the specification of the various methods for bar(). After all, that's the whole point of having a method dispatcher in the first place, right? Hence I would consider the statements as "undesired work" and I'm looking for a better way.

One could of course resort to to using NULL as default value for all "critical" arguments, but I would like to rely on missing() instead of is.null() in my functions as much as possible.


Solution

  • Here is an alternative idea. (It's broadly inspired by the sort of "computing on the language" used by many of R's model-fitting functions.)

    setMethod(
        f="foo", 
        signature=signature(x="ANY", y="ANY"), 
        definition=function(x, y, ...) {
            mc <- match.call()
            mc[[1]] <- quote(bar)
            eval(mc)
        }
    )
    
    
    foo(x="Hello")
    # 'y' is missing, but I can give you 'x':
    # [1] "Hello"
    # NULL
    
    foo(y="World")
    # 'x' is missing, but I can give you 'y':
    # [1] "World"
    # NULL
    
    foo()
    # [1] "Doing what I'm supposed to do when both args are missing"
    # NULL