CHAPTER 3
Many programming languages, including Java, C#, and Python, support an object-oriented programming (OOP) model. Unlike most programming languages, R supports three distinct OOP models called S3, S4, and Reference Class (RC). The S3 and S4 models are so named because those models were developed in the S language, the predecessor to R.
In addition to the three formal OOP models of R, you can also create an informal model that uses a list object to encapsulate data fields and associated functions (often called methods when part of an OOP class). The screenshot in Figure 20 gives an idea of where this chapter is headed.

Figure 20: Object-Oriented Programming Demo
In each section of this chapter, we’ll examine how to use a different model. In section 3.1, you’ll learn how to create an informal, lightweight form of OOP using the R list type, called list encapsulation. In section 3.2, we’ll see how to create OOP code using the old (but still useful) S3 model.
In section 3.3, you’ll learn how to create OOP code using the S4 model. And, in section 3.4, we’ll look at how to create OOP code using the RC model.
Because R list objects can hold both variables and functions, the most basic way to implement OOP is to encapsulate related data and functions in a list. The main advantage of list encapsulation OOP compared to the alternatives of S3, S4, and RC OOP is simplicity. Because list encapsulation has no mechanism to avoid name collisions, the technique is most often useful for your personal code library rather than code intended to be used by others.
Code Listing 7: List Encapsulation Model OOP
# books.R # R 3.2.4 # LE (list encapsulation) book = list( title <- NULL, year <- NULL, price <- NULL, init = function(lst, t, y, p) { lst$title <- t lst$year <- y lst$price <- p return(lst) }, display = function(lst) { cat(“Title :”, lst$title, “\n”) cat(“Year :”, lst$year, “\n”) cat(“Price : $”, lst$price, “\n”) }, setPrice = function(lst, p) { lst$price <- p return(lst) }, getPrice = function(lst) { return(lst$price) } ) # ----- cat(“\nBegin OOP with list encapsulation demo \n\n”) # initialization cat(“Initializing a ‘book’ object \n”) book <- book$init(book, “NOTITLE”, “NOYEAR”, “NOPRICE”) cat(“Object ‘book’ is: \n”) book$display(book) cat(“\n\n”) # set fields cat(“Setting ‘book’ directly and with a setter() \n”) book$title <- “A Princess of Mars” book$year <- as.integer(1912) book <- book$setPrice(book, 12.95) book$display(book) cat(“\n”) # get fields cat(“Getting title and price fields \n”) ti <- book$title pr <- book$getPrice(book) cat(“Price of ‘“) cat(ti) cat(“‘ is “, pr, “\n”) cat(“\n\n”) # assignment cat(“Calling ‘tome <- book’ \n”) tome <- book cat(“Object ‘tome’ is: \n”) tome$display(tome) cat(“\n”) cat(“Modifying title of object ‘tome’ \n”) tome$title <- “xxxxxxxx” cat(“Object ‘tome’ is: \n”) tome$display(tome) cat(“\n”) cat(“Original object ‘book’ is unchanged: \n”) book$display(book) cat(“\nEnd demo \n”) |
> source(“books.R”) Begin OOP with list encapsulation demo Initializing a ‘book’ object Object ‘book’ is: Title : NOTITLE Year : NOYEAR Price : $ NOPRICE Setting ‘book’ directly and with a setter() Title : A Princess of Mars Year : 1912 Price : $ 12.95 Getting title and price fields Price of ‘A Princess of Mars’ is 12.95 Calling ‘tome <- book’ Object ‘tome’ is: Title : A Princess of Mars Year : 1912 Price : $ 12.95 Modifying title of object ‘tome’ Object ‘tome’ is: Title : xxxxxxxx Year : 1912 Price : $ 12.95 Original object ‘book’ is unchanged: Title : A Princess of Mars Year : 1912 Price : $ 12.95 End demo |
The demo program defines a book object as a list that has a title, a year (of publication), a price, and four related functions. Notice there’s no explicit way to enforce data types—for instance, the year field could be character, integer, or numeric:
book = list(
title <- NULL,
year <- NULL,
price <- NULL,
. . .
With list encapsulation, you define a specific instance of an object rather than a template for creating objects. But, as I’ll show shortly, you can create multiple objects using copy-by-value. The book object defines a function to display itself:
display = function(lst) {
cat(“Title :”, lst$title, “\n”)
cat(“Year :”, lst$year, “\n”)
cat(“Price : $”, lst$price, “\n”)
},
The book object has an initializer function that supplies values to the three fields:
init = function(lst, t, y, p) {
lst$title <- t
lst$year <- y
lst$price <- p
return(lst)
},
Each book object function, including the init() function, has a parameter named lst that represents the book object. The purpose of this is illustrated by the calling statements:
cat(“Initializing a ‘book’ object \n”)
book <- book$init(book, “NOTITLE”, “NOYEAR”, “NOPRICE”)
cat(“Object ‘book’ is: \n”)
book$display(book)
Notice that the book object appears three times in the call to init(). This triple-reference pattern is used in list encapsulation OOP by any function call that modifies the source object. The call to the display() function needs the book object only twice because book is not changed by the function call.
The demo program defines a function that modifies the price field of the book object:
setPrice = function(lst, p) {
lst$price <- p
return(lst)
},
Function setPrice() is called this way:
cat(“Setting ‘book’ directly and with a setter() \n”)
book$title <- “A Princess of Mars”
book$year <- as.integer(1912)
book <- book$setPrice(book, 12.95)
book$display(book)
In list encapsulation, all fields have public scope, which means they can be accessed directly by using the $ operator, as shown here when the title and year fields are modified. Or, you can write a set-function and call it using the triple-reference pattern.
The demo defines an accessor get-function for the price field:
getPrice = function(lst) {
return(lst$price)
}
This is the demo program code that calls getPrice():
cat(“Getting title and price fields \n”)
ti <- book$title
pr <- book$Price(book)
cat(“Price of ‘“); cat(ti); cat(“‘ is “, pr, “\n”)
List encapsulation object fields can be accessed directly or by using a get-function. Setting field values and getting field values directly, using the $ operator, is much simpler than writing and calling set-functions and get-functions. So why use functions? My personal rule of thumb is that I write a list encapsulation OOP function only if the function uses two or more fields or performs a nontrivial calculation.
In this example, if the code is intended for actual use rather than as a demo, I would write the display() function because it uses all three book fields, but I wouldn’t write the setPrice() and getPrice() functions because they only access one field.
In order to create multiple objects, you can use this kind of ordinary assignment:
cat(“Calling ‘tome <- book’ \n”)
tome <- book
cat(“Object ‘tome’ is: \n”)
tome$display(tome)
Because R assigns by value rather than by reference, object tome would be a complete, independent copy of the book object. The independence is demonstrated in the demo by these calling statements:
cat(“Modifying title of object ‘tome’ \n”)
tome$title <- “xxxxxxxx”
cat(“Object ‘tome’ is: \n”)
tome$display(tome)
cat(“Original object ‘book’ is unchanged: \n”)
book$display(book)
The title of object tome is changed, but because tome is not associated by reference to object book, a change to tome has no effect on the book object.
In summary, the simplest possible way to implement a form of object-oriented programming in R is to create a list object that holds related data and functions. If you want to access data, you can do so directly using the $ operator or by writing get-functions and set-functions. A call to an encapsulated function that changes the source object will reference the object three times. A call to an encapsulated function that doesn’t change the source object will use the object two times. List encapsulation creates a specific instance of an object, but you can create additional objects with the same data fields and function definitions by using ordinary assignment.
Using list encapsulation OOP is essentially using R lists. For information about the list type, see:
https://stat.ethz.ch/R-manual/R-devel/library/base/html/list.html.
A program-defined function is actually called a closure in R. See:
https://stat.ethz.ch/R-manual/R-devel/library/base/html/function.html.
The R language supports an OOP model called S3. Much of the base R language was written using the S3 model. Although S3 has been replaced to some extent by the S4 and RC OOP models, S3 is perfectly viable and is the model of choice for many of my colleagues.
Code Listing 8: S3 Model OOP
# students.R # R 3.2.4 # S3 OOP Student = function(ln=“NONAME”, un=0, gpa=0.00) { this <- list( lastName = ln, units = un, gradePoint = gpa ) class(this) <- append(class(this), “Student”) return(this) } display = function(obj) { UseMethod(“display”, obj) } display.Student = function(obj) { cat(“Last name : “, obj$lastName, “\n”) cat(“Units : “, obj$units, “\n”) cat(“GPA : “, obj$gradePoint, “\n”) } setUnits = function(obj, u) { UseMethod(“setUnits”, obj) } setUnits.Student = function(obj, un) { obj$units <- un return(obj) } getUnits = function(obj) { UseMethod(“getUnits”, obj) } getUnits.Student = function(obj) { return(obj$units) } # ----- cat(“\nBegin OOP with S3 demo \n\n”) # initialization cat(“Initializing a default Student object s1 \n”) s1 <- Student() # default param values cat(“Student ‘s1’ is: \n”) display(s1) cat(“\n”) cat(“Initializing a Student object s2 \n”) s2 <- Student(“Barker”, 27, 2.87) cat(“Student ‘s2’ is: \n”) display(s2) cat(“\n”) # set fields cat(“Setting s1 directly and with a setter() \n”) s1$lastName <- “Archer” s1 <- setUnits(s1, 19) s1$gradePoint <- 1.99 cat(“Student ‘s1’ is now: \n”) display(s1) cat(“\n”) # get fields cat(“Getting lastName and units for ‘s1’ \n”) ln <- s1$lastName units <- getUnits(s1) cat(“Units for Student ‘“) cat(ln) cat(“‘ are “, units, “\n”) cat(“\n”) # assignment cat(“Calling ‘s3 <- s1’ \n”) s3 <- s1 cat(“Object ‘s3’ is: \n”) display(s3) cat(“\n”) cat(“Modifying all fields of object ‘s3’ \n”) s3$lastName <- “Coogan” s3 <- setUnits(s3, 38) s3$gradePoint <- 3.08 cat(“Object ‘s3’ is now: \n”) display(s3) cat(“\n”) cat(“Original object ‘s1’ is unchanged: \n”) display(s1) cat(“\nEnd demo \n”) |
> source(“students.R”) Begin OOP with S3 demo Initializing a default Student object s1 Student ‘s1’ is: Last name : NONAME Units : 0 GPA : 0 Initializing a Student object s2 Student ‘s2’ is: Last name : Barker Units : 27 GPA : 2.87 Setting s1 directly and with a setter() Student ‘s1’ is now: Last name : Archer Units : 19 GPA : 1.99 Getting lastName and units for ‘s1’ Units for Student ‘Archer’ are 19 Calling ‘s3 <- s1’ Object ‘s3’ is: Last name : Archer Units : 19 GPA : 1.99 Modifying all fields of object ‘s3’ Object ‘s3’ is now: Last name : Coogan Units : 38 GPA : 3.08 Original object ‘s1’ is unchanged: Last name : Archer Units : 19 GPA : 1.99 End demo |
The S3 OOP model is significantly different from the models used by other programing languages and even by the other R OOP models. If you examine the code listing, you’ll see that S3 is essentially a collection of logically related but physically independent functions.
The demo program defines a Student class. The core definition is a single function named Student(), which is defined as:
Student = function(ln=“NONAME”, un=0, gpa=0.00) {
this <- list(
lastName = ln,
units = un,
gradePoint = gpa
)
class(this) <- append(class(this), “Student”)
return(this)
}
The Student() function contains a list that holds the field variables of lastName, units, and gradePoint. The list is assigned an object with the name this, but in S3 you can use any legal name for the field list. Common alternatives to this are self (as used in Python) and me (as used in Visual Basic).
The final two statements in the definition of function Student(), which use the keywords class and append, are, in my opinion, best thought of as an S3 magic incantation.
The demo program creates a first Student named s1 with this code:
cat(“Initializing a default Student object s1 \n”)
s1 <- Student()
cat(“Student ‘s1’ is: \n”)
display(s1)
Creating an S3 object looks exactly like calling an ordinary R function. Because no parameter values are passed to Student(), the default parameter values of “NONAME,” 0, and 0.0 will be used for s1 fields lastName, units, and gradePoint.
Notice that the display() function is called as though it were a built-in part of the R language, as opposed to a call such as s1.display() that you’d see in other programming languages. The S3 model registers functions with the global R runtime environment so that they can be called as if they were built-in functions.
The display() function is first registered, then defined, this way:
display = function(obj) {
UseMethod(“display”, obj)
}
display.Student = function(obj) {
cat(“Last name : “, obj$lastName, “\n”)
cat(“Units : “, obj$units, “\n”)
cat(“GPA : “, obj$gradePoint, “\n”)
}
The registration mechanism uses the UseMethod() function to tell R that a custom function named display() will be defined. The display() function is associated with the S3 Student class by using the naming convention of functionName.className. If you’ve used OOP in other languages, the S3 model may seem a bit odd at first.
The demo program defines a function named setUnits() in order to modify the units fields of a Student object:
setUnits = function(obj, un) {
UseMethod(“setUnits”, obj)
}
setUnits.Student = function(obj, un) {
obj$units <- un
return(obj)
}
Again, the register-define pattern is used. Notice that because function setUnits() modifies its source Student object, the function must return the object. Function setUnits() is called with these statements:
cat(“Setting s1 directly and with a setter() \n”)
s1$lastName <- “Archer”
s1 <- setUnits(s1, 19)
s1$gradePoint <- 1.99
cat(“Student ‘s1’ is now: \n”)
display(s1)
An S3 object can be modified directly using the $ operator or by using a set-function. Notice that in order to call a function that changes an object, you must use the object <- function(object, value) pattern.
S3 objects are copied by value so that if you use assignment, the new object will be an independent copy and any changes made to one of the objects will not affect the other. For example:
s3 <- s1 # create a copy of s1
s3$lastName <- “Coogan” # modify s3
s3 <- setUnits(s3, 38)
s3$gradePoint <- 3.08
display(s1) # s1 has not been affected
In summary, S3 objects are defined as a collection of related functions. Each object has an initialization function containing a list that holds the fields. Auxiliary functions are defined in pairs, with one function to register the auxiliary function and one to define the function’s behavior. Functions on S3 objects are called as if they were built-in functions. Functions that change an S3 object must be called using the object <- function(object, value) pattern.
For detailed information about the UseMethod() function, see:
https://stat.ethz.ch/R-manual/R-devel/library/base/html/UseMethod.html.
In 2001, the R language introduced an OOP model called S4 that is quite a bit more sophisticated than the earlier S3 model. However, for many programming scenarios, the advanced features available in S4 (such as a form of inheritance and checking for input validity) are optional and not needed.
Code Listing 9: S4 Model OOP
# persons.R # R 3.2.4 # S4 OOP Person = setClass( “Person”, slots = list( lastName = “character”, age = “integer”, payRate = “numeric” ), prototype = list( lastName = “NONAME”, age = as.integer(-1), payRate = 0.00 ) # could define validity() here ) setGeneric(name=“display”, def=function(obj) { standardGeneric(“display”) } ) setMethod(f=“display”, signature=“Person”, definition=function(obj) { cat(“Last Name :”, obj@lastName, “\n”) cat(“Age :”, obj@age, “\n”) cat(“Pay Rate : $”, obj@payRate, “\n”) } ) setGeneric(name=“setPayRate”, def=function(obj, pRate) { standardGeneric(“setPayRate”) } ) setMethod(f=“setPayRate”, signature=“Person”, definition=function(obj, pRate) { obj@payRate <- pRate return(obj) } ) setGeneric(name=“getPayRate”, def=function(obj) { standardGeneric(“getPayRate”) } ) setMethod(f=“getPayRate”, signature=“Person”, definition=function(obj) { return(obj@payRate) } ) # ----- cat(“\nBegin OOP with S4 demo \n\n”) # initialization cat(“Initializing a blank Person object p1 \n”) p1 <- new(“Person”) # calls prototype() # p1 <- Person() # non-preferred cat(“Person ‘p1’ is: \n”) display(p1) cat(“\n”) cat(“Initializing a Person object p2 \n”) p2 <- new(“Person”, lastName=“Baker”, age=as.integer(22), payRate=22.22) cat(“Person ‘p2’ is: \n”) display(p2) cat(“\n”) # set fields cat(“Setting ‘p1’ directly and with a setter() \n”) p1@lastName <- “Adams” p1@age <- as.integer(18) p1 <- setPayRate(p1, 11.11) cat(“Person ‘p1’ is now: \n”) display(p1) cat(“\n”) # get fields cat(“Getting lastName and payRate of ‘p1’ \n”) ln <- p1@lastName pr <- getPayRate(p1) cat(“Pay rate of ‘“) cat(ln) cat(“‘ is “, pr, “\n”) cat(“\n”) # assignment cat(“Calling ‘p3 <- p1’ \n”) p3 <- p1 cat(“Object ‘p3’ is: \n”) display(p3) cat(“\n”) cat(“Modifying lastName, payRate of object ‘p3’ \n”) p3@lastName <- “Chang” p3 <- setPayRate(p3, 33.33) cat(“Object ‘p3’ is now: \n”) display(p3) cat(“\n”) cat(“Original object ‘p1’ is unchanged: \n”) display(p1) cat(“\nEnd demo \n”) |
> source(“persons.R”) Begin OOP with S4 demo Initializing a blank Person object p1 Person ‘p1’ is: Last Name : NONAME Age : -1 Pay Rate : $ 0 Initializing a Person object p2 Person ‘p2’ is: Last Name : Baker Age : 22 Pay Rate : $ 22.22 Setting ‘p1’ directly and with a setter() Person ‘p1’ is now: Last Name : Adams Age : 18 Pay Rate : $ 11.11 Getting lastName and payRate of ‘p1’ Pay rate of ‘Adams’ is 11.11 Calling ‘p3 <- p1’ Object ‘p3’ is: Last Name : Adams Age : 18 Pay Rate : $ 11.11 Modifying lastName, payRate of object ‘p3’ Object ‘p3’ is now: Last Name : Chang Age : 18 Pay Rate : $ 33.33 Original object ‘p1’ is unchanged: Last Name : Adams Age : 18 Pay Rate : $ 11.11 End demo |
The S4 OOP model is significantly different from the models used by other programing languages, but S4 does share some similarities with the S3 model. If you examine the code listing, you’ll see that S4, like S3, is essentially a collection of logically related but physically independent functions.
The demo program defines an S4 Person class using the setClass() function:
Person = setClass(
“Person”,
slots = list(
lastName = “character”,
age = “integer”,
payRate = “numeric”
),
prototype = list(
lastName = “NONAME”,
age = as.integer(-1),
payRate = 0.00
)
)
The class fields are defined in a list object with the name slots. You can specify the type of each field, which will allow R to perform validation checks. Using a representation parameter offers an alternative to slots, but the R documentation recommends using slots.
The optional prototype parameter is used to assign default values to an object. For example, the demo program creates a Person object named p1 using the new() function this way:
cat(“Initializing a blank Person object p1 \n”)
p1 <- new(“Person”)
Because no values are specified for the lastName, age, and payRate fields, the default values of “NONAME,” -1, and 0.00 specified in the prototype will be used. Object p1 could have been created with the statement p1 <- Person(), but I recommend using the new() function.
Creating functions that are associated with an S4 class requires a call to setGeneric() to register the function name and a call to setMethod() to define the associated function:
setGeneric(name=“setPayRate”,
def=function(obj, pRate) {
standardGeneric(“setPayRate”)
}
)
setMethod(f=“setPayRate”,
signature=“Person”,
definition=function(obj, pRate) {
obj@payRate <- pRate
return(obj)
}
)
This two-step process is a bit cumbersome, and the syntax isn’t obvious. Notice that because the setPayRate() function modifies the source Person object, the function must return a reference to the object. The setPayRate() function can be called this way:
p1@lastName <- “Adams”
p1@age <- as.integer(18)
p1 <- setPayRate(p1, 11.11)
The fields of an S4 object can be accessed directly by using the @ operator or by defining a set-function. S4 objects are copied by value so that assignment creates an independent copy:
p2 <- new(“Person”, lastName=“Baker”, age=as.integer(22), payRate=22.22)
p1 <- p2 # any change to p1 or p2 leaves the other unchanged
In summary, an S4 class is defined using the setClass() function, which can accept a slots object that defines class fields and their types, and an optional prototype object that defines default field values. An S4 object is instantiated using the new() method. You can directly access fields using the @ operator or by writing get-functions and set-functions. Each function associated with a class is defined by a setGeneric() function that registers the associated function and also with a setMethod() function that contains the associated function behavior definition.
For detailed information about creating S4 classes with the setClass() function, see:
https://stat.ethz.ch/R-manual/R-devel/library/methods/html/setClass.html.
For additional information about the S4 prototype() function for S4 object initialization, see:
https://stat.ethz.ch/R-manual/R-devel/library/methods/html/representation.html.
Remember that the R language has an OOP model called Reference Class (RC). RC is quite similar to the OOP models used by languages such as C#, Python, and Java. In my opinion, the RC model is significantly superior to the older S3 and S4 models; in fact, RC is my preferred model for most R programming scenarios when I use OOP.
Code Listing 10: Reference Class (RC) Model OOP
# movies.R # R 3.2.4 # RC (Reference Class) OOP Movie <- setRefClass( “Movie”, fields = list( title = “character”, released = “character”, # release date runTime = “integer” ), methods = list( display = function() { cat(“Title :”, title, “\n”) cat(“Release date :”, released, “\n”) cat(“Run time :”, runTime, “\n”) }, getAge = function() { rd = as.POSIXlt(released) today = as.POSIXlt(Sys.Date()) age = today$year - rd$year if (today$mon < rd$mon || (today$mon == rd$mon && today$mday < rd$mday)) { return(age - 1) } else { return(age) } }, setReleased = function(rd) { released <<- rd # .self$released <- rd } ) # methods ) # ----- cat(“\nBegin OOP using RC demo \n\n”) # initialization cat(“Initializing a blank Movie object using $new() \n”) m1 <- Movie$new() cat(“Movie ‘m1’ is: \n”) m1$display() cat(“\n”) cat(“Initializing a Movie object m2 \n”) m2 <- Movie$new(title=“Blade Runner”, released=“1982-06-25”, runTime=as.integer(117)) cat(“Movie ‘m2’ is: \n”) m2$display() cat(“\n”) # set fields cat(“Setting ‘m1’ directly and with a setter() \n”) m1$title <- “Alien” m1$setReleased(“1979-05-25”) m1$runTime <- as.integer(117) cat(“Movie ‘m1’ is now: \n”) m1$display() cat(“\n”) # get fields cat(“Getting title and age of ‘p1’ \n”) ti <- m1$title age <- m1$getAge() cat(“The age of ‘“) cat(ti) cat(“‘ is “, age, “years\n”) cat(“\n”) # assignment cat(“Calling m3 <- m1$copy() to make a copy \n”) m3 <- m1$copy() cat(“Object ‘m3’ is: \n”) m3$display() cat(“\n”) cat(“Modifying all fields of object ‘m3’ \n”) m3$title <- “Cube” m3$setReleased(“1997-09-09”) m3$runTime <- as.integer(90) cat(“Object ‘m3’ is now: \n”) m3$display() cat(“\n”) cat(“Original object ‘m1’ is unchanged: \n”) cat(“Object m1 is: \n”) m1$display() cat(“\n”) cat(“Calling ‘m4 <- m1’ (probably wrong!) \n”) m4 <- m1 cat(“Object ‘m4’ is: \n”) m4$display() cat(“\n”) cat(“Modifying all fields of object ‘m4’ \n”) m4$title <- “Dark City” m4$setReleased(“1998-02-27”) m4$runTime <- as.integer(100) cat(“Object ‘m4’ is now: \n”) m4$display() cat(“\n”) cat(“BUT original object ‘m1’ has changed too: \n”) cat(“Object m1 is: \n”) m1$display() cat(“\nEnd demo \n”) |
> source(“movies.R”) Begin OOP using RC demo Initializing a blank Movie object using $new() Movie ‘m1’ is: Title : Release date : Run time : Initializing a Movie object m2 Movie ‘m2’ is: Title : Blade Runner Release date : 1982-06-25 Run time : 117 Setting ‘m1’ directly and with a setter() Movie ‘m1’ is now: Title : Alien Release date : 1979-05-25 Run time : 117 Getting title and age of ‘p1’ The age of ‘Alien’ is 36 years Calling m3 <- m1$copy() to make a copy Object ‘m3’ is: Title : Alien Release date : 1979-05-25 Run time : 117 Modifying all fields of object ‘m3’ Object ‘m3’ is now: Title : Cube Release date : 1997-09-09 Run time : 90 Original object ‘m1’ is unchanged: Object m1 is: Title : Alien Release date : 1979-05-25 Run time : 117 Calling ‘m4 <- m1’ (probably wrong!) Object ‘m4’ is: Title : Alien Release date : 1979-05-25 Run time : 117 Modifying all fields of object ‘m4’ Object ‘m4’ is now: Title : Dark City Release date : 1998-02-27 Run time : 100 BUT original object ‘m1’ has changed too: Object m1 is: Title : Dark City Release date : 1998-02-27 Run time : 100 End demo |
The demo program defines an RC Movie class using the setRefClass() function. The structure of the call to setRefClass() is:
Movie <- setRefClass(
“Movie”,
fields = list(
# fields go here
),
methods = list(
# class functions go here
)
)
Unlike S3 and S4 classes, RC class functions (technically called methods) are physically defined inside the class definition. The Movie class fields are:
fields = list(
title = “character”,
released = “character”, # release date
runTime = “integer”
),
The data types of the fields in an RC class are specified, and this allows R to perform runtime error checking.
The Movie class has three associated functions/methods. The first is a display function defined as:
display = function() {
cat(“Title :”, title, “\n”)
cat(“Release date :”, released, “\n”)
cat(“Run time :”, runTime, “\n”)
},
The class fields title, released, and runTime can be accessed directly by the display() function. And you can make the field membership explicit by using the special .self variable with the $ operator. For example:
cat(“Title :”, .self$title, “\n”)
The Movie class has a getAge() function that returns the age, in years, of a Movie object by calculating the difference between the current date and the release date:
getAge = function() {
rd = as.POSIXlt(released)
today = as.POSIXlt(Sys.Date())
age = today$year - rd$year
if (today$mon < rd$mon || (today$mon == rd$mon && today$mday < rd$mday)) {
return(age - 1)
} else {
return(age)
}
},
The getAge() function uses the built-in as.POSIXlt() function to convert the released field and the system date-time to a structure that has year, mon, and mday fields. The year fields are subtracted in order to get the age of the Movie object, although the age must be decremented by one year if the month and day of the release date are less than those of the system date (because the movie’s “birthday” hasn’t happened yet).
The third Movie class function is one that allows you to set the release date:
setReleased = function(rd) {
released <<- rd
}
In order to modify an RC class field via a member function, you must use the special <<- operator rather than the regular <- or = assignment operators. This is very important—forgetting to use <<- is a common source of RC class implementation errors.
The demo program creates a Movie object using these statements:
cat(“Initializing a blank Movie object using $new() \n”)
m1 <- Movie$new()
cat(“Movie ‘m1’ is: \n”)
m1$display()
The pattern to create an RC object is object <- className$new(). Here, because no values are passed to new(), object m1 has no values for the title, released, and runTime fields. These values must be supplied later, either directly or by using a set-function, as I’ll demonstrate shortly.
Calling a member function/method of an RC object uses the pattern object$function(). This pattern is significantly different from the S3 and S4 calling patterns. For example, if a Movie class is defined using the S3 or S4 model, a display function will be called as display(m1).
The demo creates a second Movie object with field values:
cat(“Initializing a Movie object m2 \n”)
m2 <- Movie$new(title=“Blade Runner”, released=“1982-06-25”, runTime=as.integer(117))
cat(“Movie ‘m2’ is: \n”)
m2$display()
When instantiating an RC object, if you have not defined the optional initialize() function, you cannot pass unnamed arguments to new(). For example:
m2 <- Movie$new(“Blade Runner”, “1982-06-25”, as.integer(117)) # wrong
This code will run, but it will generate a waning message that “unnamed arguments to $new() must be objects from a reference class,” and the class fields will not get values.
The demo program assigns values to the instantiated but uninitialized m1 object this way:
cat(“Setting ‘m1’ directly and with a setter() \n”)
m1$title <- “Alien”
m1$setReleased(“1979-05-25”)
m1$runTime <- as.integer(117)
All RC object fields have public visibility, which means fields can be accessed directly using the $ operator. Some programming languages, such as Java and C#, allow you to define private fields and functions, but other languages, including R and Python, do not. A recently released add-on package named R6 allows you to define R classes that can have private fields and functions.
Unlike all other R objects and variables, RC objects are copied by reference rather than by value. This means you must be careful when using assignment with RC objects—you’ll typically want to use the built-in copy() function. The idea is best explained by example. Suppose the Movie object m1 has been instantiated and initialized. This code is probably not what you want:
m4 <- m1 # probably not what you intend
m4$title <- “Dark City”
m4$setReleased(“1998-02-27”)
m4$runTime <- as.integer(100)
# all fields of object m1 have also been changed too!
All RC objects inherit a built-in copy() function that will make a value copy instead of a reference copy:
m4 <- m1$copy() # probably what you want to do
m4$title <- “Dark City”
m4$setReleased(“1998-02-27”)
m4$runTime <- as.integer(100)
# m1 is not affected
In summary, the Reference Class OOP model has some similarities to the OOP models used by languages such as C#, Java, and Python. You define an RC class using the setRefClass() function. All fields and class functions/methods are encapsulated in the class definition. This means you call class functions using the object$function() pattern rather than the function(object) pattern used by S3 and S4 objects. When defining a class function that modifies a field, you must use the special <<- assignment operator rather than the <- operator. All RC object fields have public visibility and can be accessed using the $ operator. RC objects are copied by reference rather than by value, so that in most cases you’ll want to use the built-in copy() function when performing object assignment.
For detailed information about RC classes, including class inheritance, see:
https://stat.ethz.ch/R-manual/R-devel/library/methods/html/refClass.html.
For additional information about the many options available when creating an RC class, see:
https://stat.ethz.ch/R-manual/R-devel/library/methods/html/setClass.html.