Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inconsistency of S4 dispatch behavior for R6 classes

Actual questions

  1. 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?

  2. 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?

Background and example

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:

  1. Turn class R6 into a formal class via setOldClass()
  2. Have all instances of R6 classes inherit from some other superclass, say, .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"’

Conclusion

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.

like image 551
Rappster Avatar asked Feb 11 '23 09:02

Rappster


1 Answers

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)

like image 170
Rappster Avatar answered Feb 27 '23 02:02

Rappster