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")
)
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).leading to
I don't know if this fits the rest of your scenario.