Old-School OOP in R: S3

If you want to implement a complex project in R, you should use S4 objects and classes. As we saw above, S4 classes implement many features of modern object-oriented programming languages: formal class definitions, simple and multiple inheritance, parameteric polymorphism, and encapsulation. Unfortunately, S3 classes are implemented and used differently from S4 objects and don’t implement many features that enable good software engineering practices.

Unfortunately, it’s very hard to avoid S3 objects in R because many important and commonly used R functions were written before S4 objects were implemented. For example, most of the modeling tools in the statistics package were written with S3 objects. In order to understand, modify, or extend this software, you have to know how S3 classes are implemented.

S3 classes are implemented through object attributes. An S3 object is simply a primitive R object with additional attributes, including a class name. There is no formal definition for an S3 object; you can manually change the attributes, including the class. (S3 objects are very similar to objects in prototype-based languages such as JavaScript.)

Above, we used time series as an example of an S4 class. There is an existing S3 class for representing time series, called “ts” objects. Let’s create a sample time series object and look at how it is implemented. Specifically, we’ll look at the attributes of the object and then use typeof and unclass to examine the underlying object:

> my.ts <- ts(data=c(1, 2, 3, 4, 5), start=c(2009, 2), frequency=12)
> my.ts
     Feb Mar Apr May Jun
2009   1   2   3   4   5
> attributes(my.ts)
$tsp
[1] 2009.083 2009.417   12.000

$class
[1] "ts"

> typeof(my.ts)
[1] "double"
> unclass(my.ts)
[1] 1 2 3 4 5
attr(,"tsp")
[1] 2009.083 2009.417   12.000

As you can see, a ts object is just a numeric vector (of doubles), with two attributes: class and tsp. The class attribute is just the name “ts,” and the tsp attribute is just a vector with a start time, end time, and frequency. You can’t access attributes in an S3 object using the same operator that you use to access slots in an S4 object:

> my.ts@tsp
Error: trying to get slot "tsp" from an object (class "ts")
       that is not an S4 object

S3 classes lack the structure of S3 objects. Inheritance is implemented informally, and encapsulation is not enforced by the language.[31] S3 classes also don’t allow parametric polymorphism. S3 classes do, however, allow simple polymorphism. It is possible to define S3 generic functions and to dispatch by object type.

S3 generic functions work by naming convention, not by explicitly registering methods for different classes. Here is how to create a generic function using S3 classes:

Rather than fabricating an example, let’s look at an S3 generic function in R: plot:

> plot
function (x, y, ...)
UseMethod("plot")
<bytecode: 0x106c21140>
<environment: namespace:graphics>

When you call plot on a function, plot calls UseMethod("plot"). UseMethod looks at the class of the object x. It then looks for a function named plot.class and calls plot.class(x, y, ...).

For example, we defined a new TimeSeries class above. To add a plot method for TimeSeries objects, we simply create a function named plot.TimeSeries:

> plot.TimeSeries <- function(object, ...) {
+   plot(object@data, ...)
+ }

So we could now call:

> plot(my.TimeSeries)

and R would, in turn, call plot.TimeSeries(my.TimeSeries).

The function UseMethod dispatches to the appropriate method, depending on the class of the first argument’s calling function. UseMethod iterates through each class in the object’s class vector, until it finds a suitable method. If it finds no suitable method, UseMethod looks for a function for the class “default.” (A closely related function, NextMethod, is used in a method called by UseMethod; it calls the next available method for an object. See the help file for more information.)

You can’t specify an S3 class for a slot in an S4 class. To use an S3 class as a slot in an S4 class, you need to create an S4 class based on the S3 class. A simple way to do this is through the function setOldClass:

setOldClass(Classes, prototype, where, test = FALSE, S4Class)

This function takes the following arguments.

ArgumentDescriptionDefault
ClassesA character vector specifying the names of the old-style classes. 
prototypeAn object to use as a prototype; this will be used as the default object for the S4 class. 
whereAn environment specifying where to store the class definition.The top-level environment
testA logical value specifying whether to explicitly test inheritance for the object. Specify test=TRUE if there can be multiple inheritance.FALSE
S4ClassA class definition for an S4 class or a class name for an S4 class. This will be used to define the new class. 

Sometimes, you may encounter cases where individual methods are hidden. The author of a package may choose to hide individual methods in order to encapsulate details of the implementation within the package; hiding methods encourages you to use the generic functions. For example, individual methods for the generic method histogram (in the lattice package) are hidden:

> library(lattice)
> methods(histogram)
[1] histogram.factor*  histogram.formula* histogram.numeric*

   Nonvisible functions are asterisked > histogram.factor()
Error: could not find function "histogram.factor"

Sometimes, you might want to retrieve the hidden methods (for example, to view the R code). To retrieve the hidden method, use the function getS3method. For example, to fetch the code for histogram.formula, try the following command:

> getS3method(f="histogram", class="formula")

Alternatively, you can use the function getAnywhere:

> getAnywhere(histogram.formula)


[31] If the attribute class is a vector with more than one element, then the first element is interpreted as the class of the object, and other elements name classes that the object “inherits” from. That makes inheritance a property of objects, not classes.