2

Actual question

How can I turn a bunch of R6 classes that inherit from each other into S4 classes while preserving the inheritance structure when all classes need to live in a package's namespace (as opposed to in GlobalEnv)?

Details

Everything works fine in cases where the R6 classes have been defined in .GlobalEnv (as when sourcing with source()) and setOldClass() is also called with where = .GlobalEnv.

But I cannot get it to work when the R6 classes have been defined inside a package's namespace (as when calling devtools::load_all()):

Defining the R6 classes in .GlobalEnv:

Object <- R6Class("Object", portable = TRUE, public = list(
  foo = function() "foo")
)
Api <- R6Class("Api", inherit = Object, portable = TRUE,
  public = list(bar = function() "bar")
)
Module <- R6Class("Module", inherit = Api, portable = TRUE,
  public = list(fooBar = function() "fooBar")

Calling setOldClass() with where = .GlobalEnv (the default for where):

setOldClass(c("Object", "R6"))
setOldClass(c("Api", "Object"))
setOldClass(c("Module", "Api"))

When the R6 classes are defined inside a package's namespace (as when "sourcing" with devtools::load_all() instead of source()), I assumed that I need to account for that by providing an explicit where:

where <- if ("package:r6.s4" %in% search()) {
  as.environment("package:r6.s4")
} else {
  .GlobalEnv
}
try(setOldClass(c("Object", "R6"), where = where))
try(setOldClass(c("Api", "Object"), where = where))
try(setOldClass(c("Module", "Api"), where = where))

However, that leaves me with the following error:

Error in setOldClass(c("Module", "Api"), where = where) : inconsistent old-style class information for “Module”; the class is defined but does not extend “Api” and is not valid as the data part


Facilitating reproducability

I tried to make this issue as easily reproducible as possible, so you'll find the r6.s4 package at my GitHub repository

Note again that you have to run devtools::load_all() (or hit CRTL + SHFT + L in RStudio) in order to reproduce the error.

Also, this unit test might help in figuring out what's going on.

Rappster
  • 12,762
  • 7
  • 71
  • 120

1 Answers1

2

I think I got it figured out.

Lessons learned

  1. The reason setOldClass(c("Module", "Api")) fails is due to the fact that package Rcpp has a class with identical name defined.

    require("R6")
    > getClass("Module")
    Class "Module" [package "Rcpp"]
    
    Slots:
    
      Name:       .xData
    Class: environment
    
    Extends: 
      Class ".environment", directly
    Class "environment", by class ".environment", distance 2, with explicit coerce
    Class "refObject", by class ".environment", distance 3, with explicit coerce
    
  2. The best place to call setOldClass() seems to be inside .onAttach() as at this stage the package has been fully loaded and thus a namespace environment exists that the where argument can point to.

    .onAttach <- function(libname, pkgname) {
      where <- as.environment("package:r6.s4")
      clss <- list(
        c("Object", "R6"),
        c("Api", "Object"),
        c("Module2", "Api")
      )
      sapply(clss, function(cls) {
        try(setOldClass(cls, where = where))
      })
    }
    
  3. Inside .onAttach(), you need to be carful with not "overloading" classes that have been set by setOldClass() on previous package loads. That's why something along the lines of this might make sense:

     .onAttach <- function(libname, pkgname) {
      where <- as.environment("package:r6.s4")
      clss <- list(
        c("Object", "R6"),
        c("Api", "Object"),
        c("Module2", "Api")
      )
      sapply(clss, function(cls) {
        idx <- sapply(cls, isClass)
        try(sapply(cls[idx], removeClass, where = where))
        try(setOldClass(cls, where = where))
      })      
    }
    
  4. I like the approach of R6 relying on actual generator objects instead of mere class names a lot as it empowers you to use :: and thus keep classes as organized as all of your other package components. But that paradigm unfortunately seems to be lost when registering S4 equivalents for them via setOldClass(). That kind of takes me back to my old complaint about the increasing risk of name clashes for classes - *sigh*.

For those of you interested in the details of my trial-and-error process: I've tried to turn the package into sort of a self-reference. Check files R/classes.r, tests/testthat/test-S4.r as well as some prototype code on how to check for and handle name clashes R/name_clashes.r.

Community
  • 1
  • 1
Rappster
  • 12,762
  • 7
  • 71
  • 120
  • I have a similar situation. But in my package I wrote some methods for the classes defined in the package (i.e. `Api` and `Module2` in your example), for a generic S4 function defined in some other package (e.g. `sp::coordinates`) using something like: `setMethod('coordinates', signature = 'Api') { ... }`. This results in an error when loading the package, because the class `Api` is not registered at the time where `R` parses the new method. – ƒacu.- Jun 10 '15 at 09:12