15.2 Reference Classes with Package R6
The package R6 (Chang 2020) provides access to R6 classes. You will need to attach it:
library(R6)
We’ll get started by creating a miniature computerized world of objects that is based on characters from the Wizard of Oz.
15.2.1 Defining a Class
Construction of the object-world begins with the definition of classes. A class is simply a general prototype on the basis of which actual objects may be created.
Let’s define the class Person
. This is accomplished with the R6Class()
function:
<- R6Class(
Person classname = "Person",
public = list(
name = NULL,
age = NULL,
desire = NULL,
initialize = function(name = NA, age = NA, desire = NA) {
$name <- name
self$age <- age
self$desire <- desire
self$greet()
self
},set_age = function(val) {
$age <- val
selfcat("Age of ", self$name, " is: ", val, ".\n", sep = "")
},set_desire = function(val) {
$desire <- val
selfcat("Desire of ", self$name, " is: ", val, ".\n", sep = "")
},greet = function() {
cat(paste0("Hello, my name is ", self$name, ".\n"))
}
) )
Let’s analyze the above code. The function R6Class()
has a quite a few parameters, but the two with which we will concern ourselves the most are:
classname
: the name we propose to give to the class;public
: a list of members of the class that are “public” in the sense that they can be accessed and used outside of the class.36 Members are of two types:- attributes: the objects
name
,age
anddesire
. Currently these have valueNULL
, but they can be given other values later on. What makes then attributes, though, is that they will not be given a function as a value. - methods: these are members of the class that are functions. You can think of a “method” as a particular way of performing a common task that could be performed in many different ways. The four methods you see in
Person
are:initialize
: This is a function that will be run whenever a new object of class Person is created. As we can see from its arguments, it will be possible to give values to the attributesname
.age
anddesire
when this function is called.set_age
andset_desire
are functions that allow a user to set or to change the value ofage
anddesire
for an object of class Person.greet
: a function that will permit an object of class Person to issue a greeting.
- attributes: the objects
Note the use of the term self
in the code for the methods of the class. When a method is called on an object, the term self
will refer to the object on which the method is being called. Hence, for example, in the code for greet
the term self$name
will evaluate to the name of the person who issues the greeting.
15.2.2 Instantiation: Initializing Objects
On its own there is not much that a class can do. For anything to happen we need to create a particular individual: an object of class Person. Creation of an object having a particular class is called instantiation.
Every class comes with a method—not mentioned in the definition of the class—called new()
. This method, when called on the Class, instantiates an object of the class by running the initialize()
function. Let’s create a person named Dorothy and store this new object in the variable dorothy
:
<- Person$new(
dorothy name = "Dorothy", age = 12,
desire = "Kansas"
)
## Hello, my name is Dorothy.
Note how the dollar-sign is used to indicate the calling of the new()
method on the class Person. Note also that the arguments of new()
are the arguments of initialize()
: that’s because the new()
method actually runs initialize()
as part of its object-creation process.
We can get a look at dorothy
by printing her to the console:
dorothy
## <Person>
## Public:
## age: 12
## clone: function (deep = FALSE)
## desire: Kansas
## greet: function ()
## initialize: function (name = NA, age = NA, desire = NA)
## name: Dorothy
## set_age: function (val)
## set_desire: function (val)
We get the basic information on Dorothy, including also an indication that she can be cloned (copied). We’ll discuss cloning later.
We can instantiate as many people as we like. The code below for example, establishes scarecrow
as a new instance of Person:
<- Person$new(
scarecrow name = "Scarecrow", age = 0.038,
desire = "Brains"
)
## Hello, my name is Scarecrow.
In The Wizard of Oz, Scarecrow is only two weeks old when Dorothy meets him, so his age is set at \(2/52 \approx 0.038\) years.
15.2.3 Getting and Setting Attributes
If we would like to change Dorothy’s age, we can do so by calling the set_age()
method on her:
$set_age(13) dorothy
## Age of Dorothy is: 13.
$age dorothy
## [1] 13
We can also set Dorothy’s age directly, by regular assignment:
$age <- 14 dorothy
$age dorothy
## [1] 14
The effect is the same as when we use set_age()
except that we don’t get a report to the console.
15.2.4 Calling Methods
We have seen that in order to call a method on an object, you follow the format:
$method() object
Thus, we can ask dorothy
to issue a greeting:
$greet() dorothy
## Hello, my name is Dorothy.
In the syntax for calling methods, we see one aspect of “message-passing”: dorothy$greet()
essentially passes a message to dorothy
: “Please call your greet()
method!”
15.2.5 Holding a Reference, not a Value
R6 objects operate by what are known in computer programming as reference semantics. This means that when the assignment operator is used to assign an R6 object to a variable, the new variable holds a reference to that object, not a copy of the new object.
This is in contrast value semantics, which is the way assignment usually works in R. In value semantics, R a distinct copy of the value of the assigned object is created, and the new variable refers to that copy. Below is an example of the familiar value semantics in action:
<- 10
a <- a
b <- 20
b a
## [1] 10
b
has been changed to 20, but that change did not affect a
, which keeps its initial value of 10.
We can use the function address()
from the package pryr (Wickham 2018) to track what is happening behind the scenes. address()
will tell us the current location in memory of the value corresponding to a name. Let’s repeat the above process, but use address()
to see where the values are stored:
<- 10
a ::address(a) pryr
## [1] "0x7fc37a7c59e0"
The value 10 is stored in the memory address given above. Next, let’s create b
by assignment from a
:
<- a
b ::address(b) pryr
## [1] "0x7fc37a7c59e0"
For the moment b
points to the same place in memory as a
does, so it will yield 10:
b
## [1] 10
We can use b
for other operations; for example, we can add 30 to it:
+ 30 b
## [1] 40
But for now b
still points to the same place in memory that a
does:
::address(b) pryr
## [1] "0x7fc37a7c59e0"
But now let’s assign a new value to b
:
<- 20
b ::address(b) pryr
## [1] "0x7fc37743c168"
Aha! b
now points to a new location in the computer’s memory! That’s because R saw that the assignment operator was going to change the value of b
. Since b
has value semantics, R knows to set aside a new spot in memory to contain the value 20, and to associate the name b
with that spot. That way, the new b
won’t interfere with a
, which points to the same old spot in memory and thus remains 10:
::address(a) pryr
## [1] "0x7fc37a7c59e0"
a
## [1] 10
On the other hand, R6 objects have reference semantics. We can see this in action with the following example. First, let’s check on dorothy
’s age:
$age dorothy
## [1] 14
Let’s now create dorothy2
by assignment from dorothy
:
<- dorothy dorothy2
Let’s check the memory locations:
c(pryr::address(dorothy), pryr::address(dorothy2))
## [1] "0x7fc39120aeb8" "0x7fc39120aeb8"
As with a
and b
, the two names initially point to the same place in memory.
Now let’s change the age of dorothy2
to 30:
$age <- 30 dorothy2
Let’s check the age of dorothy
:
$age dorothy
## [1] 30
Whoa! Changing the age of dorothy2
changed the age of dorothy
! That’s because of the reference semantics: dorothy2
continues to be associated with the same spot in memory as dorothy
, even after we begin to make changes to it:
c(pryr::address(dorothy), pryr::address(dorothy2))
## [1] "0x7fc39120aeb8" "0x7fc39120aeb8"
15.2.6 Cloning an Object
For the sorts of complex objects created by R6 classes, reference semantics can be a useful feature. But what if we want a new and truly distinct copy of an R6 object? For this we need the clone()
method that was alluded to earlier:
<- dorothy$clone()
dorothy2 dorothy2
## <Person>
## Public:
## age: 30
## clone: function (deep = FALSE)
## desire: Kansas
## greet: function ()
## initialize: function (name = NA, age = NA, desire = NA)
## name: Dorothy
## set_age: function (val)
## set_desire: function (val)
dorothy2
looks just like dorothy
. However, the name dorothy2
does not point to the same place in memory:
c(pryr::address(dorothy), pryr::address(dorothy2))
## [1] "0x7fc39120aeb8" "0x7fc3884a3350"
Accordingly, changes to dorothy2
will no longer result in changes to dorothy
:
$age <- 100
dorothy2$age dorothy
## [1] 30
You should know that if one or more of the members of an object is itself an object with reference semantics (such as an instance of an R6 class), then a copy produced by clone()
will hold a reference to that same member-object, not a separate copy of the member-object. In such a case if you still want a totally separate copy, you will have to consult the R6 manual on the topic of “deep cloning.”
R6Class()
has another parameter calledprivate
, which takes as value a list of members that can only be accessed within the class by members of the class, not by programmers working outside of the class itself. In the course of development of very large and complex programs, it can be useful to keep some members private so that programmers don’t accidentally change too much about the way objects work. Since our programs are still on the small side, we won’t worry about private members, for now.↩︎