Chapter 10: Object-Oriented Programming
Shih Chien University
2026-08-03
At heart R is a functional language — and writing functional programs is fine practice. But R also supports object-oriented programming (OOP), the dominant paradigm of Java, C#, Ruby, C++… Many R packages are built with R objects, including the core stats package, lattice, and ggplot2. OOP shines when representing complicated things: statistical models, charts.
Two mechanisms coexist, both inherited from S:
The book’s advice: build new abstractions with S4; learn S3 because you must read and extend the vast code that uses it.
A time series — equally spaced measurements over time, with a start, end, frequency — makes every OOP concept concrete:
period) doing the right thing for different classes is polymorphism;(Yes, R already has a time-series class — ts, an S3 class in stats. That is deliberate: we will rebuild it the S4 way, then dissect the S3 original.)
Slots are where an object stores information. We represent a time series by three slots — the data, a start time, an end time (units, frequency, and period are derivable):
The representation names the class of object each slot holds.
new is the generic constructor: first argument the class name, the rest fill the slots. The default S4 print method shows every slot:
An object of class "TimeSeries"
Slot "data":
[1] 1 2 3 4 5 6
Slot "start":
[1] "2009-07-01 GMT"
Slot "end":
[1] "2009-07-01 00:05:00 GMT"
Not all slot values make sense: end must not precede start, and both must have length 1. Register a validity function with setValidity and check with validObject:
Class "TimeSeries" [in ".GlobalEnv"]
Slots:
Name: data start end
Class: numeric POSIXct POSIXct
[1] TRUE
(You can also supply the validity function when calling setClass — see its full definition.)
From now on, new checks each candidate and rejects the invalid:
good.TimeSeries <- new("TimeSeries",
data=c(7, 8, 9, 10, 11, 12),
start=as.POSIXct("07/01/2009 0:06:00", tz="GMT",
format="%m/%d/%Y %H:%M:%S"),
end=as.POSIXct("07/01/2009 0:11:00", tz="GMT",
format="%m/%d/%Y %H:%M:%S"))
bad.TimeSeries <- new("TimeSeries",
data=c(7, 8, 9, 10, 11, 12),
start=as.POSIXct("07/01/2009 0:06:00", tz="GMT",
format="%m/%d/%Y %H:%M:%S"),
end=as.POSIXct("07/01/1999 0:11:00", tz="GMT",
format="%m/%d/%Y %H:%M:%S")) # ends before it starts!Error in `validObject()`:
! 類別為 "TimeSeries" 的物件無效: FALSE
A plain function that computes the period from the slots:
The @ operator reaches into slots — the implementer’s view. Users should get nicer verbs; enter generic functions.
To extract the data series from any suitable object — polymorphism — define a function and promote it to a generic; the old body becomes the default method:
[1] "series"
[1] 1 2 3 4 5 6
Function: series (package .GlobalEnv)
object="ANY"
object="TimeSeries"
(inherited from: object="ANY")
(In OOP terms this is overloading the function name.)
Create a generic period, then register period.TimeSeries as its method for the TimeSeries signature:
[1] "period"
Time difference of 1 mins
Calling the generic dispatches to the right definition by class.
You can attach methods to generics that already exist, like summary — or to operators:
[1] "2009-07-01 to 2009-07-01 00:05:00"
[1] "1,2,3,4,5,6"
[1] 3
Note
One trap: auto-printing of S4 objects goes through show(), not print — define a show method to change how objects display at the console. ?S4groupGeneric documents the group generics (Arith, Compare, Ops, …); the S3 section below explains more.
A weight history is a time series plus a person’s name and height. contains declares the superclass; the new class adds only its extra slots:
setClass("WeightHistory",
representation(height = "numeric", name = "character"),
contains = "TimeSeries")
john.doe <- new("WeightHistory",
data=c(170, 169, 171, 168, 170, 169),
start=as.POSIXct("02/14/2009 0:00:00", tz="GMT",
format="%m/%d/%Y %H:%M:%S"),
end=as.POSIXct("03/28/2009 0:00:00", tz="GMT",
format="%m/%d/%Y %H:%M:%S"),
height=72, name="John Doe")R validates the embedded TimeSeries automatically (try an invalid one yourself).
A cleaner composition: define a Person class, then inherit from both:
Identical behavior, slightly cleaner design — and AltWeightHistory now inherits methods from both parents.
Add a Cat class — cats have names too. To write one is.fluffy method covering people and cats, create a virtual class above both with setClassUnion:
Methods can target NamedThing; objects cannot be created from it (its representation is ambiguous). A subclass method (say, is.fluffy for Person) would override the parent’s. Bonus — class membership is testable:
[1] TRUE
[1] TRUE
| Argument | Description |
|---|---|
Class |
name of the new class (the only required argument) |
representation |
named list of slots and their classes ("ANY" allows anything) |
prototype |
object holding default slot values |
contains |
names of superclasses this class extends |
validity |
validity-checking function (default: none; changeable via setValidity) |
where |
environment to store the definition (default: where setClass was called) |
sealed |
may the class be redefined by another setClass? |
package |
package name for the class |
S3methods |
may S3 methods be written for this class? (default FALSE) |
access, version |
unused; S-PLUS compatibility |
Helpers representation() and prototype() (in methods) ease definitions that extend a basic type, have several superclasses, or mix both.
class, comment, dim, dimnames, names, row.names, tsp. (Objects can have both slots and attributes.).Data slot holding the basic part — code written for the built-in type keeps working, acting on .Data.setIs(class1, class2, ...) declares inheritance explicitly — an alternative to contains.initialize method if defined — typically to compute derived slots.setClassUnion(name, members, where) creates the virtual superclass seen earlier.new(c, ...) — the constructor: named arguments fill slots; an initialize method (if any) runs afterward.object@slotname, or equivalently slot(object, "slotname").Object inspection vocabulary:
is(o, c) — is object o a member of class c? extends(c1, c2) — does class c1 extend c2?slotNames(o) / slotNames(c) — slot names of an object / class; getSlots(c) — the classes of each slot (nonintuitively).Conversion: as(o, c) coerces object o to class c. For your own classes, register coercion methods with setAs:
| Argument | Description |
|---|---|
from, to |
class names of input and output |
def |
function performing the conversion |
replace |
function for the replacement form (when as is assigned into) |
where |
environment for the definition |
setGeneric(name, def=, group=list(), valueClass=character(), where=, package=, signature=, useAsDefault=, ...) — key arguments:
name / def — name and optional defining function; group — group generic (see ?S4groupGeneric);valueClass — class(es) the return value must belong to; signature — formal-argument names and classes ("ANY" = anything);useAsDefault — which function becomes the default method.setMethod(f, signature=character(), definition, where=, valueClass=NULL, sealed=FALSE):
f — generic (or its name); signature — argument classes to match; definition — the function to call; sealed — prevent redefinition.Bookkeeping functions from the methods package:
| Function | Description |
|---|---|
isGeneric / isGroup |
does a generic / group generic of this name exist? |
removeGeneric / removeMethods |
remove a generic with its methods / just the methods |
getMethod, selectMethod |
fetch the method for a function + signature |
existsMethod, hasMethod |
does such a method exist? |
findMethod |
which package(s) contain it? |
showMethods |
display all methods of an S4 generic |
dumpMethod / dumpMethods |
dump one / all methods to code |
findFunction |
where on the search list a function is defined |
More help: library(help="methods").
Classes of the built-in types — everything is built on these, and you may write methods for them:
| Category | Object type | Class |
|---|---|---|
| Vectors | integer / double / complex / character / logical / raw |
integer / numeric / complex / character / logical / raw |
| Compound | list / pairlist / environment |
list / pairlist / environment |
| Special | NULL |
NULL |
| Language | symbol / language / expression |
name / call / expression |
| Functions | closure / special / builtin |
function |
| Other | externalptr |
externalptr |
The six vector classes all extend the virtual class vector.
An S3 object is a primitive object wearing extra attributes — one of them named class. No formal definition exists; you can change the attributes by hand. (S3 resembles prototype-based languages like JavaScript.) Dissecting the built-in ts class:
Feb Mar Apr May Jun
2009 1 2 3 4 5
$tsp
[1] 2009.083 2009.417 12.000
$class
[1] "ts"
[1] "double"
[1] 1 2 3 4 5
attr(,"tsp")
[1] 2009.083 2009.417 12.000
A ts is a double vector plus class and tsp attributes. And S4 tools don’t apply:
S3 inheritance is informal — a class attribute with several elements means the first is the class, the rest are “inherited” classes, making inheritance a property of objects, not classes. Encapsulation is unenforced; parametric polymorphism is absent. Simple polymorphism, however, works — via S3 generics.
S3 generics dispatch by naming convention, not registration:
gname;gname with body UseMethod("gname");gname.classname whose first argument is such an object.The real thing:
UseMethod walks the object’s class vector looking for plot.class; failing everything, it tries plot.default. (NextMethod, callable inside a method, hands off to the next available method.)
Give TimeSeries a plot method by merely naming a function correctly:
plot(my.TimeSeries) quietly becomes plot.TimeSeries(my.TimeSeries).
S4 slots cannot hold an S3 class directly — first wrap it with setOldClass(Classes, prototype, where, test=FALSE, S4Class):
Classes — name(s) of the old-style class;prototype — default object for the S4 class;test — set TRUE if there can be multiple inheritance;S4Class — an S4 class definition to base the new class on.Package authors may hide individual methods to push you toward the generic — encapsulation by obscurity:
[1] histogram.data.frame* histogram.factor* histogram.formula*
[4] histogram.numeric*
see '?methods' for accessing help and source code
Error in `histogram.factor()`:
! 沒有這個函式 "histogram.factor"
To read the hidden code anyway:
Tip
Modern footnote. Today’s ecosystem adds further systems — R6 for mutable reference objects and S7 as the planned successor unifying S3/S4. The concepts you just learned (classes, methods, dispatch, inheritance) transfer directly.
Copyright. These slides are adapted from R in a Nutshell: A Desktop Quick Reference (2nd ed.) by Joseph Adler, O’Reilly Media. All rights reserved by the original author and publisher.
Non-commercial use only. These materials are strictly for educational purposes and may not be used for commercial gain.
Attribution. Any reproduction, distribution, or use of these materials must properly credit the original source.
R in a Nutshell: A Desktop Quick Reference