Vectors of S4 classes with non-trivial slots

Here’s another rabbit hole where I spent a bit of time this evening. I like OOP and I like the way R uses vectors. I’ve created a few classes and had started to code a function which would plot a set of them. It all seemed straightforward until I realized that the infrastructure for treating your own classes is something that you have to build. Moreover, if you have a slot with an object like a data frame, this could get sticky.

Here’s a toy example. Let’s say I’d like to keep track of movies that I borrow from friends. I can do that by establishing a data frame to record the films and I’ve got a slot to indicate from whom I borrowed them.

movie = c("Thunderball", "Goldfinger")
rating = c(4,5)
dfJoe = data.frame(movie = movie, rating = rating)

movie = c("Manhattan", "Interiors", "Radio Days", "Bananas")
rating = c(5, 4, 3, 5)
dfBob = data.frame(movie = movie, rating = rating)

setClass("BorrowedStuff", representation(stuff = "data.frame", from="character"))

JoesStuff = new("BorrowedStuff", from = "Joe", stuff = dfJoe)

BobsStuff = new("BorrowedStuff", from = "Bob", stuff = dfBob)

I now have two objects which store a set of relevant information (relevant to Joe and Bob, at any rate). But surely I’d like to keep this all in one place. We do that with vectors. When you pass a vector into a function- even an S4 new class method- it will pass the arguments in as vectors. The function will attempt to evaluate the output using vector operations and- probably- return a vector. Here’s a silly example.

sillyFunction = function(x){
  x + 1


Note the second call to the function will return a vector. So, if we want a vector of S4 objects, we just pass in vectors, right? That would work for primitive datatypes, but if we try it with something like a data frame, it will fail.

whatStuff = new("BorrowedStuff", from = c("Joe", "Bob"), stuff = c(dfJoe, dfBob))
whatStuff = new("BorrowedStuff", from = c("Joe", "Bob"), stuff = list(dfJoe, dfBob))

Neither of those will work because the new BorrowedStuff method is expecting a data frame. The only way to create a “vector” of data frames is by placing them in a list and when the constructor sees that, it will complain.

The answer is to create a “c” function which will concatenate two objects.

setMethod("c", signature(x = "BorrowedStuff"), function(x, ...){
  elements = list(x, ...)
  stuffList = list()
  for (i in 1:length(elements)){
    stuffList[i] = new("BorrowedStuff", from = slot(elements[[i]], "from"), stuff = slot(elements[[i]], "stuff"))  
  class(stuffList) = "BorrowedStuff"

whatStuff = c(JoesStuff, BobsStuff)

someStuff = whatStuff[[1]]

I’ll point out that I based this code on something similar in the source for the lubridate package. In all of the material I’ve seen about S4 so far, there’s little mention of some of the nuts and bolts about building operators like c, [, + and so forth in a way that’s appropriate for your class. Oh well. Reading Hadley’s code is a fantastic way to see how experts do R right.

Setting the class to “BorrowedStuff” may not be a wise idea. I do that because it means that the object will be shown as “BorrowedStuff” rather than “list” in R Studio’s workspace pane. That’s a dreadful reason for doing something like that, but it looks cool.

Code may be found on Gist.

Posted in R

3 thoughts on “Vectors of S4 classes with non-trivial slots

  1. Great article. Helped me a ton getting better organization on some very messy data-passing in R. I was wondering if you could point me anywhere to find a solution to one small problem (mostly only a problem if passing this code forward to anyone else). Writing the “c” function as you have, I notice that in order to add an entry to a vector of S4 objects, the existing vector must be listed first and the new entry second or the function kicks out an error. I was just hoping you might have some pointers towards an example of how to fix this problem or (even better) how to code everything so as to be able to build vectors in the usual way. (i.e. add a third entry by just passing whatStuff[3] <- KylesStuff)

    Thanks again for the article and for any help you might have on my question(s).

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s