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:
Pick a name for the generic function. We’ll call this
gname
.
Create a function named gname
. In
the body for gname
, call UseMethod("
.gname
")
For each class that you want to use with
gname
, create a function called
whose first argument is an object of class
gname.classname
classname
.
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.
and calls class
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.
Argument | Description | Default |
---|---|---|
Classes | A character vector specifying the names of the old-style classes. | |
prototype | An object to use as a prototype; this will be used as the default object for the S4 class. | |
where | An environment specifying where to store the class definition. | The top-level environment |
test | A logical value specifying whether to explicitly test inheritance for the object. Specify test=TRUE if there can be multiple inheritance. | FALSE |
S4Class | A 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.