-
Notifications
You must be signed in to change notification settings - Fork 758
OO essentials
This chapter provides a field guide for recognising and working with R's objects in the wild. R has three object oriented systems (plus the base data structures), so it can be a bit intimidating. The goal of this guide is not to make you an expert in all three systems, but to help you identify what system you're working with, and ensure you know the basics of that system.
Central to any object-oriented system are the concepts of class and method. A class defines a type of object, describing what properties it possesses, how it behaves, and how it relates to other classes. Every object must be an instance of some class. A method is a function associated with a particular type of object.
R's three OO systems differ in how objects and methods are defined:
-
S3, which implements a style of object oriented programming called generic-function OO. This is different to most programming languages, like Java, C++ and C#, which implement message-passing OO. In message-passing style, messages (methods) are sent to objects and the object determines which function to call. Typically this object has a special appearance in the method call, usually appearing before the name of the method/message: e.g.
canvas.drawRect("blue")
. S3 is different. While computations are still carried out via methods, a special type of function called a generic function decides which method to call, and calls look likedrawRect(canvas, "blue")
. S3 is a very casual system, and has no formal definition of classes. -
S4, which works simiarly to S3, but is more formal and more strict. There are two major differences to S3. S4 has formal class definitions, which describe the representation and inheritance for each class. S4 also has multiple dispatch, which means the generic function can be dispatched to a method based on the class of any number of arguments, not just one.
-
RefClasses, sometimes called R5 for short, is quite different to S3 and S4. In ref classes, methods belong to classes (not functions, like in S3 and S4), and objects are mutable. Ref classes implements message-passing OO, using
$
to separate object and method:canvas$drawRect("blue")
, and are built on top of environments and S4.
There's also one other system that's not quite OO, but it's important to mention here, and that's
-
primitive: implemented at C-level and switches between different types of base data structures. Not user extensible. This is basically equivalent to writing an R function that uses
switch(typeof(x))
This leads to four types of object in R: base objects (no OO), S3, S4 and ref classes. It's most important to understand what sort of object you have, and how method dispatch works for each type.
S3 is R's first and most simple OO system. Its age means it's the only OO system used in the base and stats packages, and its simplicity means that it's the most commonly used system in packages.
Most objects you encounter in R are likely to be S3 objects, but unfortunately you can only confirm that a process of elimination: it has a class attribute (attr(x, "class")
) but it's not a S4 object (!is.S4(x)
). This check is automated by pryr::otype()
, which provides an easy way to determine the OO system of an object:
library(pryr)
df <- data.frame(x = 1:10, y = letters[1:10])
otype(df) # A data frame is an S3 class
otype(df$x) # A numeric vector isn't
otype(df$y) # A factor is
In S3, methods are associated with functions, called generics, not objects or classes. (NB: This is different from pretty much every other modern programming language, so be aware.) To determine if a function is an S3 generic function, you can look at its source code:
mean
Any function that includes a call to UseMethod()
is an S3 generic: It's the job of UseMethod()
to finds the correct method given the input and call it. Some functions functions that don't call UseMethod()
are also S3 generics. For example, the following three functions are also S3 generics:
`[`
sum
cbind
These functions are implemented in C, not R, and some functions do S3 dispatch in C code. If you looked at the C source code for these functions you'd see a call to DispatchGroup
or DispatchOrEval
, but there's an easier way to figure it out: pryr::ftype()
:
ftype(`[`)
ftype(sum)
ftype(cbind)
Functions where method dispatch is handled in C code are called "internal generics" and you can read more about them in the documentation, ?"internal generic"
.
S3 methods use a special naming scheme: generic.class
. For example, the mean method for Date objects is called mean.Date
, and the print method for factors is called print.frame
. This is the reason that most modern style guides discourage the use of .
in function names: it makes them look like S3 methods. For example, is t.test
the test
method for t
? Similarly, the use of .
in class names can also be confusing: is print.data.frame
the print
method for data.frames
, or the print.data
method for frames
.
pryr::ftype()
knows all about these exceptions so you can use it to reliable figure out if a function is an S3 method:
ftype(t.test) # generic function for t tests
ftype(t.data.frame) # data frame method for t()
ftype(is.numeric) # naming convention for testing and coercion
ftype(as.numeric) # there are not S3 generics for is and as
You can see all the methods of a generic using the methods()
function:
methods("mean")
methods("t")
S3 is a very simple and adhoc system: there is no formal definition of a class and to make an object an instance of a class, you just set the class attribute. You can do that attr()
, class()
, or during creation with structure()
:
foo <- list()
attr(foo, "class") <- "foo"
class(foo) <- "foo"
foo <- structure(list(), class = "foo")
While class is stored as an attribute, but it's better to modify it using the class()
function, since this communicates your intent more clearly. Most S3 classes will provide a constructor function:
foo <- function(x) {
structure(list(x), class = "foo")
}
and if that exists, like for factor()
and data.frame()
, you should use it: it will make sure you're creating the class with the correct components. The convention is that constructor functions have the same name as the class, which is usually lower case.
Apart from developer supplied constructor functions, S3 has no checks for correctness. This means you can change the class of existing objects:
# Create a linear model
mod <- lm(log(mpg) ~ log(disp), data = mtcars)
class(mod)
# Turn it into a table (?!)
class(mod) <- "table"
# But unsurprisingly this doesn't work very well
print(mod)
If you've used other object oriented languages, this probably makes you feel a little queasy. But surprisingly, this doesn't cause many problems: while you can change the type of an object, you never should. R doesn't protect you from yourself: you can easily shoot yourself in the foot, but if you don't aim the gun at your foot and pull the trigger, you won't have a problem.
You can determine the class of any object using class(x)
, and check if an object inherits from a specific class using inherits(x, "classname")
. The class of an S3 object can be a vector, which describes behaviour from most specific to least specific. For example, the class of the glm()
object is c("glm", "lm")
indicating that it inherits behaviour from "lm"
.
Method dispatch in S3 is relatively simple, and most of the components are described above. S3 generics look at the class of one argument, usually the first (if not, it will be listed as the second argument to UseMethod()
). If x
had more than one class, e.g. c("foo","bar")
, UseMethod
would look for mean.foo
and if not found, it would then look for mean.bar
. As a final fallback, UseMethod
will look for a default method, mean.default
, and if that doesn't exist it will raise an error. The same approach applies regardless of how many classes an object has:
# An object with 26 classes, from "a" to "z"
z <- structure(1, class = letters)
bar <- function(x) UseMethod("bar", x)
bar.z <- function(x) "z"
bar(z)
Once UseMethod
has found the correct method, it's invoked in a special way. Rather than creating a new evaluation environment, it uses the environment of the current function call (the call to the generic), so any assignments or evaluations that were made before the call to UseMethod will be accessible to the method. The arguments that were used in the call to the generic are passed on to the method in the same order they were received.
Because methods are normal R functions, you can call them directly. However, this is just as dangerous as changing the class of an object so you shouldn't do it: please don't point a loaded gun at your foot!
bar.x <- function(x) "x"
# You can call methods directly, but you shouldn't!
bar.x(z)
# [1] "x"
S4 works in a very similar way to S3, but it is much more formal and ad-hoc. Methods still belong to functions, not classes, but:
-
Classes have a formal definition of their fields, and their superclasses (classes from which behaviour is inherited)
-
There is a specific operator,
@
, for extracting fields out of an S4 object -
Method dispatch can be based on multiple objects, not just one.
In S3, you can turn any object into an object of a particular class just by setting the class attribute. S4 is much stricter: you must define the representation of the call using setClass
, and the only way to create it is through the constructer function new
.
An S4 class has three key properties:
-
a name: an alpha-numeric string that identifies the class
-
representation: a list of slots (fields), giving their names and classes. For example, a person class might be represented by a character name and a numeric age, as follows:
representation(name = "character", age = "numeric")
-
a character vector of classes that it inherits from, or in S4 terminology, contains.
You create a class with setClass
:
setClass("Person",
representation(name = "character", age = "numeric"))
setClass("Employee",
representation(boss = "Person"), contains = "Person")
and create an instance of a class with new
:
hadley <- new("Person", name = "Hadley", age = 31)
You can usually find the documentation for a class with class?className
To access slots of an S4 object you use @
:
hadley <- new("Person", name = "Hadley", age = 33)
hadley@age
Or if you have a character string giving a slot name, you use the slot
function:
slot(hadley, "age")
S4 classes have two other commonly defined properties:
- validity method
- prototype
Note that there's some tension between the usual interactive functional style of R and the global side-effect causing S4 class definitions. In most programming languages, class definition occurs at compile-time, while object instantiation occurs at run-time - it's unusual to be able to create new classes interactively. In particular, note that the examples rely on the fact that multiple calls to setClass
with the same class name will silently override the previous definition unless the first definition is sealed with sealed = TRUE
.
This section describes the strategy for matching a call to a generic function to the correct method. If there's an exact match between the class of the objects in the call, and the signature of a method, it's easy - the generic function just calls that method. Otherwise, R will figure out the method using the following method:
-
For each argument to the function, calculate the distance between the class in the class, and the class in the signature. If they are the same, the distance is zero. If the class in the signature is a parent of the class in the call, then the distance is 1. If it's a grandparent, 2, and so on. Compute the total distance by adding together the individual distances.
-
Calculate this distance for every method. If there's a method with a unique smallest distance, use that. Otherwise, give a warning and call one of the matching methods as described below.
Note that it's possible to create methods that are ambiguous - i.e. it's not clear which method the generic should pick. In this case R will pick the method that is first alphabetically and return a warning message about the situation. In this case, it's up to the class author to fix the problem by providing more specific methods.
There are two special classes that can be used in the signature: missing
and ANY
. missing
matches the case where the argument is not supplied, and ANY
is used for setting up default methods. ANY
has the lowest possible precedence in method matching.
You can also use basic classes like numeric
, character
and matrix
. A matrix of (e.g.) characters will have class matrix
. You can also dispatch on S3 classes provided that you have made S4 aware of them by calling setOldClass
. It's also possible to dispatch on ...
under special circumstances. See ?dotsMethods
for more details.
Reference Classes (or refclasses) are new in R 2.12. They fill a long standing need for mutable objects that had previously been filled by non-core packages like R.oo
, proto
and mutatr
. While the core functionality is solid, reference classes are still under active development and some details will change. The most up-to-date documentation for Reference Classes can always be found in ?ReferenceClasses
.
There are two main differences between reference classes and S3 and S4:
- Refclass objects use message-passing OO
- Refclass objects are mutable: the usual R copy on modify semantics do not apply
These properties makes this object system behave much more like Java and C#. Surprisingly, the implementation of reference classes is almost entirely in R code - they are a combination of S4 methods and environments. This is a testament to the flexibility of S4.
Creating a new reference based class is straightforward: you use setRefClass
. Unlike setClass
from S4, you want to keep the results of that function around, because that's what you use to create new objects of that type:
# Or keep reference to class around.
Person <- setRefClass("Person")
Person$new()
A reference class has three main components, given by three arguments to setRefClass
:
-
contains
, the classes which the class inherits from. These should be other reference class objects:setRefClass("Polygon") setRefClass("Regular") # Specify parent classes setRefClass("Triangle", contains = "Polygon") setRefClass("EquilateralTriangle", contains = c("Triangle", "Regular"))
-
fields
are the equivalent of slots inS4
. They can be specified as a vector of field names, or a named list of field types:setRefClass("Polygon", fields = c("sides")) setRefClass("Polygon", fields = list(sides = "numeric"))
The most important property of refclass objects is that they are mutable, or equivalently they have reference semantics:
Polygon <- setRefClass("Polygon", fields = c("sides")) square <- Polygon$new(sides = 4) triangle <- square triangle$sides <- 3 square$sides
-
methods
are functions that operate within the context of the object and can modify its fields. These can also be added after object creation, as described below.setRefClass("Dist") setRefClass("DistUniform", c("a", "b"), "Dist", methods = list( mean <- function() { (a + b) / 2 } ))
Refclass methods are associated with objects, not with functions, and are called using the special syntax obj$method(arg1, arg2, ...)
. (You might recall we've seen this construction before when we called functions stored in a named list). Methods are also special because they can modify fields. This is different
We've also seen this construct before, when we used closures to create mutable state. Reference classes work in a similar manner but give us some extra functionality:
- inheritance
- a way of documenting methods
- a way of specifying fields and their types
-
base: usually not possible. Sometimes special function available e.g.
.subset2
is the default for[[
-
S3:
NextMethod()
-
S4:
callNextMethod()
-
R5:
$callSuper(...)
Three OO systems is a lot for one language, but for most R programming, S3 suffices. In R you usually create fairly simple objects, and use OO programming to add special behaviour to generic functions like print()
, summary()
and plot()
. S3 is well suited to this domain, and the majority of OO code that I have written in R is S3. S3 is also a little quirky, and relatively poorly documented elsewhere so this capture will focus on S3, only providing a broad overview of the details of S4 and reference classes. Once you have mastered S3, S4 is relatively easy to pickup - the ideas are all the same, it is just more formal, and more verbose. If you've programmed in another language, ref classes will be natural.
If you are creating more complicated systems of interrelated objects, S4 may be more appropriate. S4 is used extensively by Bioconductor. The Matrix
package by Douglas Bates and Martin Maechler is another package for which S4 is particularly well suited. It is designed to efficiently store and compute with many different special types of sparse matrix. As at version 0.999375-50 it defines 130 classes and 24 generic functions. The package is well written, well commented and fairly easy to read. The accompanying vignette gives a good overview of the structure of the package.
Reference classes differ from S3 and S4 in two important ways: these properties makes this object system behave much more like Java and C#. Surprisingly, the implementation of reference classes is almost entirely in R code - they are a combination of S4 methods and environments. Note that when using reference based classes we want to minimise side effects, and use them only where mutable state is absolutely required. The majority of functions should still be "functional", and side effect free. This makes code easier to reason about (because you don't need to worry about methods changing things in surprising ways), and easier for other R programmers to understand.