Generalizing `...` (three dots) argument dispatch: S4 methods for argument set including `...`

765 views Asked by At

Actual question

Is it possible to define methods for a set of signature arguments that includes ... (as opposed to exclusively for ...)? It's not possible "out-of-the-box", but would it theoretically be possible at all (involving some tweaks) or is this something that simply cannot be done due to the way the S4 mechanism is designed?

I'm looking for something along the lines of

setGeneric(
  name = "foo",
  signature = c("x", "..."),
  def = function(x, ...) standardGeneric("foo")      
)
setMethod(
  f = "foo", 
  signature = signature(x = "character", "..." = "ThreedotsRelevantForMe"), 
 definition = function(x, ...) bar(x = x)
)

Martin Morgan thankfully pointed me to dotsMethods and it says this:

Currently, “...” cannot be mixed with other formal arguments: either the signature of the generic function is “...” only, or it does not contain “...”. (This restriction may be lifted in a future version.)

Background

Consider the following attempt to generalize the dispatching mechanism based on ... from a simple case (only one more function is supposed to use arguments passed via ...; e.g. the use of ... in plot() for passing arguments to par()) to scenarios involving the following aspects (taken from here):

  • when you would like to pass along arguments to more than one, hence r, recipients,
  • when those recipients can be located on c different layers of the calling stack
  • and when they might even use the same argument names but associate different meanings to these arguments in their very own scope/closure/frame/environment

Also note that, while it may indeed be good practice to do so, top-level functions/interfaces should not necessarily need to be concerned with definining (lots of) explicit arguments of subsequently called functions/interfaces in order to pass arguments correctly. IMO, this choice should be left to the developer as sometimes one or the other alternative makes more sense.

It would be cool if I could substitute the dispatch that is currently handled via withThreedots() (which AFAICT would need to involve an actual split-up of ...) with the S4 dispatcher somehow, thus ideally simply being able to call foo(x = x, ...) instead of withThreedots("foo", x = x, ...) in foobar():

Definitions

withThreedots <- function(fun, ...) {
  threedots <- list(...)
  idx <- which(names(threedots) %in% sprintf("args_%s", fun))
  eval(substitute(
    do.call(FUN, c(THREE_THIS, THREE_REST)),
    list(
      FUN = as.name(fun),
      THREE_THIS = if (length(idx)) threedots[[idx]], 
      THREE_REST = if (length(idx)) threedots[-idx] else threedots
    )
  ))
}
foobar <- function(x, ...) {
  withThreedots("foo", x = x, ...)
}
foo <- function(x = x, y = "some text", ...) {
  message("foo/y")
  print(y)
  withThreedots("bar", x = x, ...)
}
bar <- function(x = x, y = 1, ...) {
  message("bar/y")
  print(y)
  withThreedots("downTheLine", x = x, ...)
}
downTheLine <- function(x = x, y = list(), ...) {
  message("downTheLine/y")
  print(y)
}

Apply

foobar(x = 10) 
foobar(x = 10, args_foo = list(y = "hello world!")) 
foobar(x = 10, args_bar = list(y = 10)) 
foobar(x = 10, args_downTheLine = list(y = list(a = TRUE))) 

foobar(x = 10, 
       args_foo = list(y = "hello world!"), 
       args_bar = list(y = 10),
       args_downTheLine = list(y = list(a = TRUE))
)

# foo/y
# [1] "hello world!"
# bar/y
# [1] 10
# downTheLine/y
# $a
# [1] TRUE

Conceptional approach (MOSTLY PSEUDO CODE)

I guess I'm looking for something along the lines of this:

Definitions

setGeneric(
  name = "foobar",
  signature = c("x"),
  def = function(x, ...) standardGeneric("foobar")
)
setMethod(
  f = "foobar", 
  signature = signature(x = "ANY"), 
  definition = function(x, ...) pkg.foo::foo(x = x, ...)
)

Assumption: foo() is defined in package/namespace pkg.foo

setGeneric(
  name = "foo",
  signature = c("x", "y", "..."),
  def = function(x, y = "some text", ...) standardGeneric("foo")      
)
setMethod(
  f = "foo", 
  signature = signature(x = "ANY", y = "character", "..." = "Threedots.pkg.foo.foo"), 
  definition = function(x, y, ...) {
    message("foo/y")
    print(y)
    pkg.bar::bar(x = x, ...)
  }
)

Assumption: bar() is defined in package/namespace pkg.bar:

setGeneric(
  name = "bar",
  signature = c("x", "y", "..."),
  def = function(x, y = 1, ...) standardGeneric("bar")      
)
setMethod(
  f = "bar", 
  signature = signature(x = "ANY", y = "numeric", "..." = "Threedots.pkg.bar.bar"), 
  definition = function(x, y, ...) {
    message("bar/y")
    print(y)
    pkg.a::downTheLine(x = x, ...)
)
setGeneric(
  name = "downTheLine",
  signature = c("x", "y", "..."),
  def = function(x, y = list(), ...) standardGeneric("downTheLine")      
)

Assumption: downTheLine() is defined in package/namespace pkg.a:

setMethod(
  f = "downTheLine", 
  signature = signature(x = "ANY", y = "list", "..." = "Threedots.pkg.a.downTheLine"), 
  definition = function(x, y, ...) {
    message("downTheLine/y")
    print(y)
    return(TRUE)
)

Illustration what the dispatcher would need to do

The crucial part is that it would have to be able to distinguish between those elements in ... that are relevant for the respective current fun being called (based on a full S4 dispatch on regular and threedots signature arguments) and those elements that should be passed along to functions that fun might be calling (i.e., an updated state of ...; similar to what's happening inside withThreedots() above):

s4Dispatcher <- function(fun, ...) {
  threedots <- splitThreedots(list(...))
  ## --> automatically split `...`:
  ## 1) into those arguments that are part of the signature list of `fun` 
  ## 2) remaining part: everything that is not part of
  ##    the signature list and that should thus be passed further along as an 
  ##    updated version of the original `...`

  args_this <- threedots$this
  ## --> actual argument set relevant for the actual call to `fun`
  threedots <- threedots$threedots
  ## --> updated `...` to be passed along to other functions

  mthd <- selectMethod(fun, signature = inferSignature(args_this))
  ## --> `inferSignature()` would need to be able to infer the correct
  ## signature vector to be passed to `selectMethod()` from `args_this`

  ## Actual call //
  do.call(mthd, c(args_this, threedots))
}

Here's an illustration of how a generator for a "typed three dots argument container" could look like.

Note that in order for such a mechanism to work across packages, it would probably make sense to also offer a possibility to state the namespace of a certain function (arg ns and field .ns):

require("R6")
Threedots <- function(..., fun, ns = NULL) {
  name <- if (!is.null(ns)) sprintf("Threedots.%s.%s", ns, fun) else 
      sprintf("Threedots.%s", fun)
  eval(substitute({
    INSTANCE <- R6Class(CLASS,
      portable = TRUE,
      public = list(
        .args = "list",     ## Argument list
        .fun = "character", ## Function name
        .ns = "character",  ## Namespace of function
        initialize = function(..., fun, ns = NULL) {
          self$.fun <- fun
          self$.ns <- ns
          self$.args <- structure(list(), names = character())
          value <- list(...)
          if (length(value)) {
            self$.args <- value
          }
        }
      )
    )
    INSTANCE$new(..., fun = fun, ns = ns)
    },
    list(CLASS = name, INSTANCE = as.name(name))
  ))
}

Example

x <- Threedots(y = "hello world!", fun = "foo", ns = "pkg.foo")

x
# <Threedots.pkg.foo.foo>
#   Public:
#     .args: list
#     .fun: foo
#     .ns: pkg.foo
#     initialize: function

class(x)
# [1] "Threedots.pkg.foo.foo" "R6" 

x$.args
# $y
# [1] "hello world!"

The actual calls would then look like this:

foobar(x = 10) 
foobar(x = 10, Threedots(y = "hello world!", fun = "foo", ns = "pkg.foo")) 
foobar(x = 10, Threedots(y = 10, fun = "bar", ns = "pkg.bar")) 
foobar(x = 10, Threedots(y = list(a = TRUE), fun = "downTheLine", ns = "pkg.a"))) 

foobar(x = 10, 
       Threedots(y = "hello world!", fun = "foo", ns = "pkg.foo"),
       Threedots(y = 10, fun = "bar", ns = "pkg.bar),
       Threedots(y = list(a = 10), fun = "downTheLine", ns = "pkg.a")
)
1

There are 1 answers

2
Martin Morgan On

See ?setGeneric and search for '...', and then ?dotsMethods. It's possible to define a generic that dispatches on ... (only, not mixed with other arguments for dispatch).

.A = setClass("A", contains="numeric")
.B = setClass("B", contains="A")

setGeneric("foo", function(...) standardGeneric("foo"))
setMethod("foo", "A", function(...) "foo,A-method")

setGeneric("bar", function(..., verbose=TRUE) standardGeneric("bar"),
           signature="...")
setMethod("bar", "A", function(..., verbose=TRUE) if (verbose) "bar,A-method")

leading to

> foo(.A(), .B())
[1] "foo,A-method"
> bar(.A(), .B())
[1] "bar,A-method"
> bar(.A(), .B(), verbose=FALSE)
> 

I don't know if this fits the rest of your scenario.