R S4 classes with the same name from different packages

Suppose that there are two packages.

Package_A has this class:

setClass("Person", 
         slots = c(
           name = "character", 
           age = "numeric"
         )
)

setGeneric("age", function(x) standardGeneric("age"))
setMethod("age", "Person", function(x) x@age)

Package_B has a similar class:

setClass("Person", 
         slots = c(
           name = "character", 
           age = "numeric"
         )
)

setGeneric("age", function(x) standardGeneric("age"))
setMethod("age", "Person", function(x) x@age * 10) # notice the difference here

So user has loaded both packages in their working environment:

library(Package_A)
library(Package_B)

In this user's working env, how does R resolve the confusion of creating a "Person" object:

john <- new("Person", name = "John Smith", age = 7)

In this user's working env, how does R resolve calling the right method:

age(john)

Upvotes: 3

Views: 230

Answers (1)

Migwell
Migwell

Reputation: 20107

Accessing classes from a specific package

getClass()

new() accepts a classRepresentation object, which you can retrieve using getClass. For example, new(getClass('className', where='packageName')). However, note that this won't work if you haven't imported the package yet, and also you have defined a new class with the same name. I demonstrate this issue here:

install.packages('lookupTable')
setClass('lookupTable', slots = c(notworking='numeric'))
new(getClass('lookupTable', where='lookupTable'))
#> An object of class "lookupTable"
#> Slot "notworking":
#> numeric(0)

(it has printed the "notworking" slot, which means it is instantiating my custom class, not the correct package version)

new(classNameWithAttribute)

There's an odd but documented feature of new, which allows you to set the package attribute on the class name, which actually works perfectly (ie doesn't have the issue mentioned above), if a bit verbosely:

name = 'className'
attr(name, 'package') = 'packageName'
new(name)

There's no reason you couldn't make this into a re-usable function though:

new = function(cls, pkg, ...){
    attr(cls, 'package') = pkg
    methods::new(cls, ...)
}
new('className', 'packageName')

Good Package Design

Of course, all of this can be avoided if the packagers of the S4 classes provide one of two mechanisms:

Exporting the setClass() value

setClass() has both a side effect (storing the class in the registry), and a return value (a class generator function). So if the packager chooses to store and export the return value in their NAMESPACE, we can access it later:

# In package "myPackage"
myClass = setClass('myClass')

# In package's NAMESPACE
export(myClass)

# In user's script
new(myPackage::myClass)

For example, you can test this out with the same test package from before:

install.packages('lookupTable')
new(lookupTable::lookupTable)

Exporting a constructor function

This is the standard practise in bioconductor. They define a constructor function that has the same name as the class itself. You can then access the constructor function using ::, and call it instead of new:

install.packages("BiocManager")
BiocManager::install("IRanges")

new("IRanges")
# Equivalent to 
IRanges::IRanges()

Upvotes: 2

Related Questions