Wishlist-for-R icon indicating copy to clipboard operation
Wishlist-for-R copied to clipboard

NextMethod() quirks: It's not a regular function - don't pass arguments!

Open HenrikBengtsson opened this issue 7 years ago • 2 comments

(Adding some old notes of mine here)

Passing arguments to NextMethod() by explicitly specifying them as one do in function calls should be avoided. The following example illustrates why. First, assume the following setup:

x <- structure(NA, class = "A")
y_truth <- list(x = x, a = 3)

foo <- function(x, a) UseMethod("foo")

foo.default <- function(x, a) {
  list(x = x, a = a)
}

Next, consider we want create a foo() method for class A that should do what the default method does except that it should increase the value of a by one. If the default method is updated, our foo() for A should not have to be updated, so we need to use NextMethod().

Attempt 1: Pass arguments by name

It's tempting to use NextMethod() as follows:

foo.A <- function(x, a) {
  tmp <- a + 1
  NextMethod("foo", object = x, a = tmp)
}

which at a first glance seems to do what we want:

y <- foo(x, a = 2)
stopifnot(identical(y, y_truth))

However, if you try to set argument a by position, we get a rather surprising error:

y <- foo(x, 2)
## Error in foo.default(x, 2, a = 3) : unused argument (a)

Hmm, that's not how we'd expect foo() to work.

Comment: It doesn't matter if we use object = x or x above.

Attempt 2: Arguments by position

Let's try to pass tmp by position instead;

foo.A <- function(x, a) {
  tmp <- a + 1
  NextMethod("foo", x, tmp)
}

Nah, that's even worse:

y <- foo(x, a = 2)
## Error in foo.default(x, a = 2) : unused argument (tmp)

y <- foo(x, 2)
Error in foo.default(x, 2) : unused argument (tmp)

Attempt 3: Arguments by position (same name)

Ok, it doesn't like tmp to be passed. What happens if we use local variable named the same as the argument and pass that instead?

foo.A <- function(x, a) {
  a <- a + 1
  NextMethod("foo", x, a)
}

You'd think that hack could do the trick, eh? Nope:

y <- foo(x, a = 2)
## Error in foo.default(x, a = 2) : unused argument (a)

y <- foo(x, 2)
## Error in foo.default(x, 2) : unused argument (a)

Attempt 4: Don't pass arguments (modify instead)

That leaves us with not passing the argument implicitly by not specifying them when calling NextMethod():

foo.A <- function(x, a) {
  a <- a + 1
  NextMethod("foo", x)
}

which works:

y <- foo(x, a = 2)
stopifnot(identical(y, y_truth))

y <- foo(x, 2)
stopifnot(identical(y, y_truth))

Cleaner version of Attempt 4

There's actually no need to specify the generic function (the first argument as a string) nor the object to dispatch on, so we can just do:

foo.A <- function(x, a) {
  a <- a + 1
  NextMethod()
}

which is easier to remember and less to update in case you rename the generic. Most importantly, it does work:

y <- foo(x, a = 2)
stopifnot(identical(y, y_truth))

y <- foo(x, 2)
stopifnot(identical(y, y_truth))

Session info

> sessionInfo()
R version 3.4.1 (2017-06-30)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 16.04.2 LTS

Matrix products: default
BLAS: /usr/lib/atlas-base/atlas/libblas.so.3.0
LAPACK: /usr/lib/atlas-base/atlas/liblapack.so.3.0

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
 [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
 [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
 [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C            
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
[1] compiler_3.4.1

See also

  • R-devel thread 'Do not pass '...' to NextMethod() - it'll do it for you; missing documentation, a bug or just me?', 2012-10-19, https://stat.ethz.ch/pipermail/r-devel/2012-October/065029.html
  • PR15654 - 'WISH/ROBUSTNESS: R CMD check for non-named '...' arguments to NextMethod()', 2014-02-03, https://bugs.r-project.org/bugzilla3/show_bug.cgi?id=15654
  • ...?

HenrikBengtsson avatar Jul 06 '17 04:07 HenrikBengtsson

NextMethod is a super weird function in general. I've discovered (through some hacking) that you can use it in place of UseMethod to define default behaviour for all subclasses, as long as the methods are not tagged as s3 methods.

Thanks for this exploration of the arguments, it has confused me many times

alanocallaghan avatar Apr 17 '18 09:04 alanocallaghan

Looking at the R source itself, we see that NextMethod() is called in several different ways, e.g.

src/library/base/R/datetime.R:        NextMethod(.Generic)
src/library/base/R/datetime.R:            return(structure(NextMethod(.Generic),
src/library/base/R/datetime.R:            return(structure(NextMethod(.Generic),
src/library/base/R/datetime.R:            structure(NextMethod(.Generic), units=u1, class = "difftime")
src/library/base/R/datetime.R:            structure(NextMethod(.Generic), units = "secs", class = "difftime")
src/library/base/R/datetime.R:               .difftime(NextMethod(), units)
src/library/base/R/datetime.R:    y <- NextMethod()
src/library/base/R/datetime.R:    NextMethod("duplicated", x)
src/library/base/demo/is.things.R:    } else NextMethod("print", ...)
src/library/parallel/R/snow.R:    v <- NextMethod()

This suggests that R core themselves don't have a standard way of using NextMethod() - maybe there's room for improvement to both the code and the docs?

HenrikBengtsson avatar May 30 '18 00:05 HenrikBengtsson