S4 Classes: Multiple Types Per Slot

S4 Classes: Multiple types per slot

R has 'class unions', so

setOldClass("data.frame")
setClassUnion("data.frameORvector", c("data.frame", "vector"))

The class data.frameORvector is virtual, so can't be instantiated but can be used in other slots (representation=), as a contained class (contains=), and for dispatch

A = setClass("A", 
representation=representation(x="data.frameORvector"))

> A(x=1:3)
An object of class "A"
Slot "x":
[1] 1 2 3

> A(x=data.frame(x=1:3, y=3:1))
An object of class "A"
Slot "x":
x y
1 1 3
2 2 2
3 3 1

Methods can be tricky to implement because all you know is that the slot contains one of the parent types of the class union.

setGeneric("hasa", function(object) standardGeneric("hasa"))
setMethod("hasa", "data.frameORvector", function(object) typeof(object@x))

> hasa(A(x=1:5))
[1] "integer"
> hasa(A(x=data.frame(y=1:5)))
[1] "list"

I actually find the documentation on ?Classes, ?Methods, ?setClass, and friends helpful. Hadley Wickham has a tutorial (the example on this page isn't that strong, it instantiates Person, whereas conceptually one would write a People to exploit R's vectorization strengths) and there is a section in this recent Bioconductor course. I don't think either goes in to detail about class unions.

How can you create multiple S4 classes with similarly named methods in R?

Try not calling setGeneric in both files.

It could be the case that the duplicate call to setGeneric creates a new generic, and gets rid of the previous one (including its associated method). This leaves you with a method only for Mynewclass.

See also
https://stat.ethz.ch/pipermail/r-devel/2010-January/056396.html

R S4 slots as lists of custom classes

For those looking for an answer to this. No, there is no way to restrict a list in S4 to have elements of certain type. This actually makes sense, since lists in R are designed to contain any type of element, so why lists on S4 should be distinct?

Efficient way to define a class with multiple, optionally-empty slots in S4 of R?

In my opinion...

Approach 2

It sort of defeats the purpose to adopt a formal class system, and then to create a class that contains ill-defined slots ('A' or NULL). At a minimum I would try to make DataClass1 have a 'NULL'-like default. As a simple example, the default here is a zero-length numeric vector.

setClass("DataClass1", representation=representation(x="numeric"))
DataClass1 <- function(x=numeric(), ...) {
new("DataClass1", x=x, ...)
}

Then

setClass("MasterClass1", representation=representation(dataClass1="DataClass1"))
MasterClass1 <- function(dataClass1=DataClass1(), ...) {
new("MasterClass1", dataClass1=dataClass1, ...)
}

One benefit of this is that methods don't have to test whether the instance in the slot is NULL or 'DataClass1'

setMethod(length, "DataClass1", function(x) length(x@x))
setMethod(length, "MasterClass1", function(x) length(x@dataClass1))

> length(MasterClass1())
[1] 0
> length(MasterClass1(DataClass1(1:5)))
[1] 5

In response to your comment about warning users when they access 'empty' slots, and remembering that users usually want functions to do something rather than tell them they're doing something wrong, I'd probably return the empty object DataClass1() which accurately reflects the state of the object. Maybe a show method would provide an overview that reinforced the status of the slot -- DataClass1: none. This seems particularly appropriate if MasterClass1 represents a way of coordinating several different analyses, of which the user may do only some.

A limitation of this approach (or your Approach 2) is that you don't get method dispatch -- you can't write methods that are appropriate only for an instance with DataClass1 instances that have non-zero length, and are forced to do some sort of manual dispatch (e.g., with if or switch). This might seem like a limitation for the developer, but it also applies to the user -- the user doesn't get a sense of which operations are uniquely appropriate to instances of MasterClass1 that have non-zero length DataClass1 instances.

Approach 1

When you say that the names of the classes in the hierarchy are going to be confusing to your user, it seems like this is maybe pointing to a more fundamental issue -- you're trying too hard to make a comprehensive representation of data types; a user will never be able to keep track of ClassWithMatrixDataFrameAndTree because it doesn't represent the way they view the data. This is maybe an opportunity to scale back your ambitions to really tackle only the most prominent parts of the area you're investigating. Or perhaps an opportunity to re-think how the user might think of and interact with the data they've collected, and to use the separation of interface (what the user sees) from implementation (how you've chosen to represent the data in classes) provided by class systems to more effectively encapsulate what the user is likely to do.

Putting the naming and number of classes aside, when you say "difficult to extend for additional data types in the future" it makes me wonder if perhaps some of the nuances of S4 classes are tripping you up? The short solution is to avoid writing your own initialize methods, and rely on the constructors to do the tricky work, along the lines of

setClass("A", representation(x="numeric"))
setClass("B", representation(y="numeric"), contains="A")

A <- function(x = numeric(), ...) new("A", x=x, ...)
B <- function(a = A(), y = numeric(), ...) new("B", a, y=y, ...)

and then

> B(A(1:5), 10)
An object of class "B"
Slot "y":
[1] 10

Slot "x":
[1] 1 2 3 4 5

Is it possible to not set types for s4 slots

Yes, you could do something like:

setClass("hi", slots = c(slot1 = "ANY"))

The use of ANY is actually documented in the help.

Using multiple constructors for R classes and subclasses

There is no reason why you can't do this with one S4 constructor by using default arguments and a little extra logic, along the lines of

setClass("Abode",
slots = list(size = "numeric")
) -> Abode

setClass("House",
slots = list(name = "character"),
contains = "Abode"
) -> House

createDwelling <- function(size=0,name,sizeString){
if(!missing(sizeString)){
if(sizeString == "Large") size <- 5
else if(sizeString == "Small") size <- 1
else stop("invalid sizeString")
}
if(missing(name)) return(Abode(size=size))
else return(House(size=size,name=name))
}

example usage:

> createDwelling(size=3)
An object of class "Abode"
Slot "size":
[1] 3

> createDwelling(sizeString="Small")
An object of class "Abode"
Slot "size":
[1] 1

> createDwelling(sizeString="Small",name="my house")
An object of class "House"
Slot "name":
[1] "my house"

Slot "size":
[1] 1

How to create an S4 class A that will have a list of S4 classes B as its attribute?

You define S4 classes with setClass, and within this you use representation to declare members and their types.

In this case, class A needs only contain a member of type list to house the collection of objects of S4 class B.

setClass("A", representation(List = "list"))
setClass("B", representation(value = "numeric"))

You declare new S4 objects with the function new, in which you specify the class name first and their members as named parameters:

my_object <- new("A", List = list(new("B", value = 1), new("B", value = 2)))
my_object
#> An object of class "A"
#> Slot "List":
#> [[1]]
#> An object of class "B"
#> Slot "value":
#> [1] 1
#>
#>
#> [[2]]
#> An object of class "B"
#> Slot "value":
#> [1] 2

We can get the List member using the @ operator:

my_object@List
#> [[1]]
#> An object of class "B"
#> Slot "value":
#> [1] 1
#>
#>
#> [[2]]
#> An object of class "B"
#> Slot "value":
#> [1] 2

From which we can access the list members, and their S4 slots directly:

my_object@List[[1]]
#> An object of class "B"
#> Slot "value":
#> [1] 1

my_object@List[[1]]@value
#> [1] 1

Created on 2020-03-16 by the reprex package (v0.3.0)

Define an S4 class with slots that have their class definitions in another package

I believe this is because mcmc is an S3 class and not a formal S4 class. You would need to use setOldClass to register the S3 as a formally defined class.

setOldClass("mcmc")
setClass(Class = "myClass", representation = representation(var = "mcmc"))


Related Topics



Leave a reply



Submit