Shouldn't the fact that R6 classes inherit from (informal S3) class R6
allow the definition of S4 methods for signature arguments of that very class?
As this is - AFAICT - not the case, what would be a workaround that is in line with current S3/S4 standards or that could somewhat be regarded as "best practice" in such situations?
Reference Classes
Consider the following example where you would like to define methods that dispatch on the superclass that all instances of Reference Classes inherit from (envRefClass
):
TestRefClass <- setRefClass("TestRefClass", fields= list(.x = "numeric"))
setGeneric("foo", signature = "x",
def = function(x) standardGeneric("foo")
)
setMethod("foo", c(x = "envRefClass"),
definition = function(x) {
"I'm the method for `envRefClass`"
})
> try(foo(x = TestRefClass$new()))
[1] "I'm the method for `envRefClass`"
This inheritance structure is not directly obvious as class()
won't reveal that fact:
class(TestRefClass$new())
[1] "TestRefClass"
attr(,"package")
[1] ".GlobalEnv"
However, a look at the attributes of the class generator object reveals it:
> attributes(TestRefClass)
[... omitted ...]
Reference Superclasses:
"envRefClass"
[... omitted ...]
That's why the dispatch works
R6 Classes
When you would like to a similar thing for R6 classes, things don't seem to be straight forward even though they initially appear so (compared to Reference Classes):
TestR6 <- R6Class("TestR6", public = list(.x = "numeric"))
setMethod("foo", c(x = "R6"),
definition = function(x) {
"I'm the method for `R6`"
})
> try(foo(x = TestR6$new()))
Error in (function (classes, fdef, mtable) :
unable to find an inherited method for function ‘foo’ for signature ‘"TestR6"’
By "appearing straight forward" I mean that class()
actually suggests that all R6 classes inherit from class R6
that could be used as superclass for method dispatch:
class(TestR6$new())
[1] "TestR6" "R6"
The help page of R6Class()
actually reveals that class R6
is merely added as an informal S3 class as long as class = TRUE
. That's also why there is a warning when trying to define a S4 method for this class.
So then this basically leaves us with two possible options/workarounds:
R6
into a formal class via setOldClass()
.R6
Ad 1)
setOldClass("R6")
> isClass("R6")
[1] TRUE
This works when hacking away in an S3 style at the class table/graph:
dummy <- structure("something", class = "R6")
> foo(dummy)
[1] "I'm the method for `R6`"
However, it fails for actual R6 class instances:
> try(foo(x = TestR6$new()))
Error in (function (classes, fdef, mtable) :
unable to find an inherited method for function ‘foo’ for signature ‘"TestR6"’
Ad 2)
.R6 <- R6Class(".R6")
TestR6_2 <- R6Class("TestR6_2", inherit = .R6, public = list(.x = "numeric"))
setMethod("foo", c(x = ".R6"),
definition = function(x) {
"I'm the method for `.R6`"
})
> try(foo(x = TestR6_2$new()))
Error in (function (classes, fdef, mtable) :
unable to find an inherited method for function ‘foo’ for signature ‘"TestR6_2"’
While approach 1 sort operates in a "grey area" to make S3 and S4 somewhat compatible, approach 2 seems like a perfectly valid "pure S4" solution that IMO should work. The fact that it's not brought me to raising the question if there exists an inconsistency in the implementation of R6 classes with respect to the interaction of informal/formal classes and method dispatch in R.
Courtesy of Hadley Wickham I found out that setOldClass()
in fact solves the problem when including the inheritance structure:
require("R6")
setOldClass(c("TestR6", "R6"))
TestR6 <- R6Class("TestR6", public = list(.x = "numeric"))
setGeneric("foo", signature = "x",
def = function(x) standardGeneric("foo")
)
setMethod("foo", c(x = "R6"),
definition = function(x) {
"I'm the method for `R6`"
})
try(foo(x = TestR6$new()))
However, AFAICT, this implies that for your packages, you need to make sure that setOldClass()
is called in that way for all of your R6 classes for which you would like your S4 methods to work.
This could be done by bundling these calls in function .onLoad()
or .onAttach()
(see here):
.onLoad <- function(libname, pkgname) {
setOldClass(c("TestR6_1", "R6"))
setOldClass(c("TestR6_2", "R6"))
setOldClass(c("TestR6_3", "R6"))
}
This is supposing that you have defined three R6 classes (TestR6_1
through TestR6_3
)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With