Wishlist-for-R
Wishlist-for-R copied to clipboard
NextMethod() quirks: It's not a regular function - don't pass arguments!
(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
- ...?
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
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?