do.call and alist: a perfect match

Author

Eliot McIntire

Published

August 17, 2020

It has been a while between posts. So, lets dive in with a simple, but technical, one.

The problem with do.call

When developing code in R, you often want to call a function with combinations of arguments that you don’t know when you write the function code. One good example is when an outer function uses an argument and then updates it for its own needs, but then passes that new argument to an inner function, e.g.,:

sumXY <- function(x, y) {
  if (missing(x)) return("x was missing")
  if (missing(y)) return("y was missing")
  x + y
}

useAndUpdateY <- function(x, ...) {
  dots <- list(...)
  if (!is.null(dots$y)) {
    message("y was provided; I want to use y and modify x as a result")
    y <- dots$y
    y <- x * y # update y
  }
  # Now, normally, we use '...' formulation in R
  sumXY(x = x, ...)
}
(out <- useAndUpdateY(x = 2, y = 3)) # Returns 5 -- Wrong!
## y was provided; I want to use y and modify x as a result
## [1] 5

Basically, this returns the wrong answer because the inner function must live with the original values of the ..., in this case, y = 3

Solution, part 1 – use do.call

We can use do.call to construct the arguments as a list, giving us immense flexibility.

sumXY <- function(x, y) {
  if (missing(x)) return("x was missing")
  if (missing(y)) return("y was missing")
  x + y
}

useAndUpdateY <- function(x, ...) {
  dots <- list(...)
  if (!is.null(dots$y)) {
    message("y was provided; I want to use y and modify x as a result")
    y <- dots$y
    y <- x * y # update y
  }
  # Now, normally, we use '...' formulation in R
  do.call(sumXY, list(x = x, y = y))
}
(out <- useAndUpdateY(x = 2, y = 3)) # returns 8! correct!
## y was provided; I want to use y and modify x as a result
## [1] 8

The main problem with this is that it evaluates list(x = x, y = y) before passing it into sumXY. For small objects, this can be unnoticeable. But for large objects, including, in our experience, all sp::SpatialPolygons objects, this can be unbearable. In interactive R sessions (including Rstudio), the user will lose access to the command prompt for minutes to hours as R attempts to print the entire object.

Solution part 2 – use alist

Basically, alist is almost identical to list, but it doesn’t evaluate the arguments before passing them along to do.call. In the help manual for ?alist, this is stated, but it is under-emphasized:

alist handles its arguments as if they described function arguments. So the values are not evaluated …

sumXY <- function(x, y) {
  if (missing(x)) return("x was missing")
  if (missing(y)) return("y was missing")
  x + y
}

useAndUpdateY <- function(x, ...) {
  dots <- list(...)
  if (!is.null(dots$y)) {
    message("y was provided; I want to use y and modify x as a result")
    y <- dots$y
    y <- x * y # update y
  }
  # Now, normally, we use '...' formulation in R
  do.call(sumXY, alist(x = x, y = y)) # update to `alist`
}
(out <- useAndUpdateY(x = 2, y = 3)) # returns 8! still correct!
## y was provided; I want to use y and modify x as a result
## [1] 8

Conclusion

Always use alist when using do.call, even for small problems.