Argument dispatch via `...` (three dots) to multiple functions across different calling stack layers

292 views Asked by At

IMO, it's not just an exact duplicate of the question that is being referred to:

My question involves a n-step dispatch (n = 2 in the actual example) of arguments in ...: bar() and foo() are not both called directly inside foobar(), but foobar() only calls foo() which in turn calls bar() and solutions as provided in the answers do not account for this. I hope the community recognizes this aspects and opens this question up.

However, I've included an answer that states my current solution which takes into account the great suggestions given in the answer to the referred question. Yet, I'd be grateful to hear about better solutions or different approaches.


I wonder if there's a clever way to fine control the exact way arguments are dispatched via R's "three dots" argument ....

Consider the following use case:

  • you have a function foobar() that calls foo() which in turn calls bar()
  • both foo() and bar() have an argument that's called y, but they each have a different meaning
  • in the call to foobar(), you would like to say "here's the y for foo() and here's the y for bar()". That's what I would like to accomplish.

If you simply call foobar(x = "John Doe", y = "hello world"), y only get's dispatched to foo() as in the call to bar() things would have to be explicit in order to be dispatched (i.e. the call would have to be bar(x = x, y = y) instead of bar(x = x, ...). Plus, it would be the "wrong" y from bar()'s perspective anyway:

foo <- function(x, y = "some character", ...) {
  message("foo ----------")
  message("foo/threedots")
  try(print(list(...)))
  message("foo/y")
  try(print(y))
  bar(x = x, ...)
}
bar <- function(x, y = TRUE, ...) {
  message("bar ----------")
  message("bar/threedots")
  try(print(list(...)))
  message("bar/y")
  try(print(y))
  return(paste0("hello: ", x))
}
foobar <- function(x, ...) {
  message("foobar ----------")
  message("foobar/threedots")
  try(print(list(...)))
  foo(x = x, ...)
}

foobar(x = "John Doe", y = "hi there")
# foobar ----------
# foobar/threedots
# $y
# [1] "hi there"
# 
# foo ----------
# foo/threedots
# list()
# foo/y
# [1] "hi there"
# bar ----------
# bar/threedots
# list()
# bar/y
# [1] TRUE
# [1] "hello: John Doe"

What I conceptionally would like to be able to do is something like this:

foobar(x = "John Doe", y_foo = "hello world!", y_bar = FALSE)

Here's an approach that works but that also feels very odd:

foo <- function(x, y = "some character", ...) {
  message("foo ----------")
  message("foo/threedots")
  try(print(list(...)))
  message("foo/y")
  arg <- paste0("y_", sys.call()[[1]])
  if (arg %in% names(list(...))) {
    y <- list(...)[[arg]]
  }
  try(print(y))
  bar(x = x, ...)
}
bar <- function(x, y = TRUE, ...) {
  message("bar ----------")
  message("bar/threedots")
  try(print(list(...)))
  message("bar/y")
  arg <- paste0("y_", sys.call()[[1]])
  if (arg %in% names(list(...))) {
    y <- list(...)[[arg]]
  }
  try(print(y))
  return(paste0("hello: ", x))
}

foobar(x = "John Doe", y_foo = "hello world!", y_bar = FALSE)
# foobar ----------
# foobar/threedots
# $y_foo
# [1] "hello world!"
# 
# $y_bar
# [1] FALSE
# 
# foo ----------
# foo/threedots
# $y_foo
# [1] "hello world!"
# 
# $y_bar
# [1] FALSE
# 
# foo/y
# [1] "hello world!"
# bar ----------
# bar/threedots
# $y_foo
# [1] "hello world!"
# 
# $y_bar
# [1] FALSE
# 
# bar/y
# [1] FALSE
# [1] "hello: John Doe"

How would you go about implementing something like this?

I also played around with S4 method dispatch to see if I could define methods for a signature argument ..., but that didn't go too well (and it's probably a very bad idea anyway):

setGeneric(
  name = "foo",
  signature = c("x", "..."),
  def = function(x, ...) standardGeneric("foo")      
)
setMethod(
  f = "foo", 
  signature = signature(x = "character", "..." = "MyThreeDotsForAFunctionImCalling"), 
 definition = function(x, ...) bar(x = x)
)
## --> does not work
1

There are 1 answers

0
Rappster On

Current solution

Note that if possible I wouldn't want foobar(), foo() or bar() to contain any explicit arguments of subsequently called functions (e.g. y_foo or args_bar). They should just be able to take any inputs that subsequently called functions can process and pass them along accordingly. Of course this would need to be clearly and well documented for the respective functions.

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
    )
  ))
}

#' @title
#' Does something foobar
#'
#' @description 
#' Calls \code{\link[foo.package]{foo}}.
#' 
#' @section Argument dispatch via ...:
#' 
#' Calling subsequent functions is handled by function 
#' \code{\link{withThreedots}}. In order for it to dispatch the correct 
#' arguments to the various functions further down the calling stack, 
#' you need to wrap them in a individual lists and name them according to 
#' the following convention: \code{args_<function-name>}. 
#' 
#' For example, arguments that should be passed to 
#' \code{\link[foo.package]{foo} would need to be stated as follows:
#' \code{args_foo = list(y = "hello world!")}. The same goes for arguments
#' that \code{\link[foo.package]{foo} passes to its subsequent functions.
#'     
#' @param x \code{\link{character}}. Some argument.
#' @param ... Further arguments to be passed to subsequent functions.
#'    In particular:
#'    \itemize{
#'      \item{\code{\link[foo.package]{foo}}. Make sure to also check if 
#'        this function in turn can pass along arguments via \code{...}. 
#'        In this case, you can also include those arguments.}
#'    }
#'    See section \strong{Argument dispatch via ...} for details about the 
#'    expected object structure of things to pass via \code{...}.
#' @example inst/examples/existsNested.r
#' @seealso \code{\link[foo.package]{foo}}
#' @export 
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

Using the built-in S4 dispatcher

What would really be neat is if one could define S4 methods for ... and have the built-in dispatcher do the job that withThreedots() currently does. Instead of a list args_<function> one would use class instances along the line of Threedots$new(<args-list>, <function-name>). Does anyone know if something like this could be done?