Object Oriented Programming (OOP) in R -- S4

Xinlei Mi,

PhD. Candidate,

Department of Biostatistics,

University of Florida

xlmi@ufl.edu

PART I: Preliminary

S3/S4 History

History of the S language.

  • First discussion at Bell labs in May 1976 for a new system to interface a large Fortran library.
  • By the end of 1976, first implementation of S.
  • Later ported to UNIX systems and becomes the S versoin 2.
  • About ten years after the first meeting, a new version with concepts inspired from UNIX system is developed with focus on functional programming and with object self-description. This is the S version 3.
  • Around 1992 is introduced the concept of classes and methods as known today by S4 classes.

A brief overview of S3

An S3 class is defined by the special attribute class, a character string vector.

In [1]:
x <- seq(0, 10, 2); y <- x + rnorm(length(x))
dat <- rbind(y, x)
attr(dat, "class") <- "foo"
# class(dat) <- "foo" #### Alternatively, use class()
# dat <- structure(rbind(y, x), class = "foo")
dat
       [,1]     [,2]     [,3]    [,4]     [,5]     [,6]
y -2.417804 1.179992 4.684105 7.46096 7.169126 11.99842
x  0.000000 2.000000 4.000000 6.00000 8.000000 10.00000
attr(,"class")
[1] "foo"
In [2]:
attr(dat, "c.names") <- paste("C", 1:length(x), sep='') # Add new attributes
dat
       [,1]     [,2]     [,3]    [,4]     [,5]     [,6]
y -2.417804 1.179992 4.684105 7.46096 7.169126 11.99842
x  0.000000 2.000000 4.000000 6.00000 8.000000 10.00000
attr(,"class")
[1] "foo"
attr(,"c.names")
[1] "C1" "C2" "C3" "C4" "C5" "C6"

S3 methods

  • Methods of generic functions are defined: (generic name).(class)
  • S3 generics: print(), plot(), ...
In [3]:
head(methods(print))
  1. 'print.acf'
  2. 'print.AES'
  3. 'print.anova'
  4. 'print.aov'
  5. 'print.aovlist'
  6. 'print.ar'
In [4]:
print.foo <- function(x, ...) {
    y <- data.frame(matrix(x, nrow(x)))
    colnames(y) <- attr(x, "c.names")
    
    cat("Print 'foo' \n")
    print(y)
}
print(dat)
Print 'foo' 
         C1       C2       C3      C4       C5       C6
1 -2.417804 1.179992 4.684105 7.46096 7.169126 11.99842
2  0.000000 2.000000 4.000000 6.00000 8.000000 10.00000

Drawbacks of S3 Classes

  • It does not check the consistency of the class
  • It has no control on inheritance
  • S3 methods can only dispatch the first argument

Introduction to S4

  • What is S4?
    • S4 is the 4th version of S. S is a language that has two implementations: S-plus in commercial, R is free.
  • What is OOP?
    • An object is a set of variables and functions which all concern the same topic: the object itself.
    • e.g., an object image will contain the variables which make it possible to define an image (as the size of the image, its mode of compression, the image itself) and the functions used to handle image (like blackAndWhite(), or resizing()).

Let's take a quick example to compare traditional programming with OOP.

The $BMI$ is a measurement of thinness or obesity. And $BMI = \frac{w}{h^2}$ where $w$ is the weight and $h$ is the size.

  • 20 < BMI < 25: Everything is fine
  • 25 < BMI < 30: Teddy bear
  • 30 < BMI < 40: Comfortable Teddy bear
  • 40 < BMI: Huge Teddy bear, with double softness effect, but who should go to see a doctor very quickly...
  • 18 < BMI < 20: Barbie
  • 16 < BMI < 18: Barbie model
  • BMI < 16: Barbie skeleton, same diagnosis as the huge Teddy bear, be careful...
In [5]:
### Traditional programming, BMI
weight <- 62
size <- 1.70
(BMI <- weight/size**2)
21.4532871972318
In [6]:
### Traditional programming, my BMI
weightMe <- 62
sizeMe <- 1.70
(BMIMe <- weightMe/sizeMe**2)
21.4532871972318
In [7]:
### Traditional programming, his BMI
weightHim <- 85
sizeHim <- 1.84
(BMIHim <- weightMe/sizeHim**2)
18.312854442344
In [8]:
### Definition of an object BMI
setClass("BMI", slots = list(weight = "numeric", size = "numeric"))
setMethod("show", "BMI",
          function(object) {
            cat("BMI =", object@weight/object@size**2, " \n")
          }
)
'show'
In [9]:
(myBMI <- new("BMI", weight = 62, size = 1.70))
BMI = 21.45329  
In [10]:
(hisBMI <- new("BMI", weight = 85, size = 1.84))
BMI = 25.10633  
In [11]:
(weight <- "hello")
'hello'
In [12]:
new("BMI", weight="hello", size=1.70)
Error in validObject(.Object): invalid class “BMI” object: invalid object for slot "weight" in class "BMI": got class "character", should be or extend class "numeric"
Traceback:

1. new("BMI", weight = "hello", size = 1.7)
2. initialize(value, ...)
3. initialize(value, ...)
4. validObject(.Object)
5. stop(msg, ": ", errors, domain = NA)
In [13]:
(sizeMe <- -1.70)
-1.7
In [14]:
### Object programming, control
setValidity("BMI",
            function(object) {
              if (object@size < 0) {
                return("Negative size not accepted. ")
              } else TRUE
            }
)
new("BMI", weight = 62, size = -1.7)
Class "BMI" [in ".GlobalEnv"]

Slots:
                      
Name:   weight    size
Class: numeric numeric
Error in validObject(.Object): invalid class “BMI” object: Negative size not accepted. 
Traceback:

1. new("BMI", weight = 62, size = -1.7)
2. initialize(value, ...)
3. initialize(value, ...)
4. validObject(.Object)
5. stop(msg, ": ", errors, domain = NA)
In [15]:
### Definition of the heir
setClass("BMIplus", slots = list(sex = "character"), contains = "BMI")
she <- new("BMIplus", size = 1.65, weight = 52, sex = "Female")
she
BMI = 19.10009  

Summary

Object programming "forces" the programmer to have a preliminary reflection. It is less possible to write "quick-and-dirty" code, a programming plan is almost essential. In particular:

  • The objects must be declared and typed.
  • Control machanisms enable to check the internal coherence of the object.
  • An object can inherit the properties which were defined for another object.
  • Lastly, the object allows an encapsulation of the program: once an object and the functions attached to it are defined, one does not need to deal with the object internal tool anymore.

Formal definition

An object is a coherent set of variables and functions which revolve around a central concept. Formally, an object is defined through three elements:

  • The class is the name of the object. It is also its architecture (the list of variables and functions which compose it).
  • The varibales of the object are called slots.
  • The functions of the object are called methods.
  • In the introduction, we had defined an object of class BMI. Its slots were weight and size, its method was show.

Slots

Slots are simply typed variables. A typed variable is a variable whose nature has been fixed.

  • In the previous example, weight, size

Methods

We distinguish 4 types of operations that can be applied on objects:

  • Creation methods: this category include all the methods use for object creation.
  • Validation: in object programming, it is possible to check if slots respect some rules.
  • Slot handling: modify and read slots is not as simple as in traditional programming. Thus, one dedicates methods to handle slot (for example names).
  • Others: the remaining methods specific to the object, in particular posting methods and calculation methods.

Drawing is winning!

A good drawing is better than a long discourse.

  • Each class is represented by a rectangle in which are noted slots and methods with their type.
  • The inheritance between two classes is noted by an arrow, from the son towads the father
In [16]:
#        ------------------------------------------
#        |                BMI                     |         --------------------
#        |----------------------------------------|         |  BMIplus         |
#        |    Slots     |                         |         |------------------|
#        |---------------                         |         | Slots  |         |
#        |      weight: numeric                   |  <---   |---------         |
#        |      size:   numeric                   |         |   sex: character |
#        |----------------------------------------|         |------------------|
#        |   Methods    |                         |         | Methods|         |
#        |---------------                         |         |---------         |
#        |      NULL <- show(object: BMI)         |         --------------------
#        ------------------------------------------

An example

  • A doctor wants to keep track of all patients' BMI over time.
  • Each patient has a sequence of BMI over time.
  • Patients are put into groups according to several criteria: gender [M/F]; surgery [Y/N/U]; ...
  • In the end, the goal is to compare various ways of grouping BMI trajectories.

This problem can be cut into three objects:

  • "bmiTraj": The first contains the patients BMI trajectories
  • "ptGroup"; The second represents the group information for patients
  • "bmiGroups": The third is a mixture of the two previous: BMI trajectories and patients' group information

The object "bmiTraj"

  • week: the week at which BMI is measured.
  • traj: the patients' BMI trajectories

Example: $$week = (1, 2, 4, 5, 7)\ \ \ \ \ traj = \left( \begin{array}{lllll} 15 & 15.1 & 15.2 & 15.2 & 14.9 \\ 16 & 15.9 & 16 & 16.4 & 16.6 \\ 15.2 & 15.2 & 15.3 & 15.3 & 15.5 \\ 15.7 & 15.6 & 15.8 & 16 & 15.7 \end{array} \right) $$

The object "ptGroup"

A ptGroup object is a set of groups. For example, the groups could be M={I1, I3} and F={I2, I4}. It is undoubtedly simpler to consider them as a vector with length the patients' number: {A, B, A, B}.

  • nbGroups: gives the number of groups.
  • group: the sequence of groups to which the BMI trajectories belong.

Example: $$nbGroups = 2\ \ \ \ \ group = \left( \begin{array}{c} M \\ F \\ M \\ F \\ \end{array} \right)$$

The object "bmiGroups"

Considering a set of BMI trajectories, several types of groups might possibly be interesting.

  • week: as in "bmiTraj"
  • traj: as in "bmiTraj"
  • groupList: a list of groups

Example: $$times = (1, 2, 4, 5, 7)\ \ \ \ \ traj = \left( \begin{array}{lllll} 15 & 15.1 & 15.2 & 15.2 & 14.9 \\ 16 & 15.9 & 16 & 16.4 & 16.6 \\ 15.2 & 15.2 & 15.3 & 15.3 & 15.5 \\ 15.7 & 15.6 & 15.8 & 16 & 15.7 \end{array} \right) $$ $$groupList = \left( \begin{array}{l} nbGroups = 3 \\ part = \left(\begin{array}{c} Y \\ N \\ U \\ Y \\ \end{array}\right) \\ \end{array} \right), \left( \begin{array}{l} nbGroups = 2 \\ part = \left(\begin{array}{c} M \\ F \\ M \\ F \\ \end{array}\right) \\ \end{array} \right)$$

Drawing

Here is the schedule of our program:

In [17]:
#      --------------------------         ----------------------
#      |     bmiTraj            |         |      ptGroup       |
#      --------------------------         ----------------------
#      | Slots |  week          |         | Slots |   nbGroups |
#      ---------  traj          |         ---------   group    |
#      --------------------------         ----------------------
#      | Methods | countMissing |         |         | Methods  |
#      ----------- print        |         ----------------------
#      |           imputation   |             ^
#      -----------------------                |
#                          /\                 |
#                          ||                 |
#                      ---------------------------
#                      |      bmiGroups          |
#                      ---------------------------
#                      | Slots |                 |
#                      --------| groupList       |
#                      ---------------------------
#                      | Methods |               |
#                      -----------  print        |
#                      ---------------------------

Application to R

  • Creation methods: the main creation method bears the name of the class.
  • Validation: That depends on the object.
  • Attributes handling: for each attributes, one will need a method giving access to its value and a method allowing to modify it.
  • Others: the other methods depend on the object characteristics, except for posting methods. For posting, 'show' allows a basic posting of the object when one types its name in the console. 'print' gives a more complete posting. 'plot' is the graphic posting.

PART II: Bases of object programming

Classes declaration

  • In most object languages, the definition of the object contains the slot and the methods.

  • In R, the definition contains only the slot. The methods are specified afterwards.

Definition of slots

The first stage is to define the slots of the object itself. That is done by using the function setClass.

  • Class is the name of the class which we are defining.
  • slots is the list of attributes of the class.
In [18]:
setClass(
  Class = "bmiTraj",
  slots = list(
    week = "numeric",
    traj = "matrix"
  )
)

Default Constructor

When a class exists, we can create an object of its class using the constructor 'new':

In [19]:
new(Class = "bmiTraj")
# new(Class = "bmiTraj", week = c(1, 3, 4))
# new(Class = "bmiTraj", week = c(1, 3), traj = matrix(1:4, 2, 2))
An object of class "bmiTraj"
Slot "week":
numeric(0)

Slot "traj":
<0 x 0 matrix>
In [20]:
# Three doctors take part to the study. Bob, Vic and Ana
bmiBob <- new(Class = "bmiTraj")
bmiVic <- new(
  Class = "bmiTraj",
  week = c(1, 3, 4, 5),
  traj = rbind(c(15, 15.1, 15.2, 15.2),
               c(16, 15.9, 16, 16.4),
               c(15.2, NA, 15.3, 15.3),
               c(15.7, 15.6, 15.8, 16))
)
bmiAna <- new(
  Class = "bmiTraj",
  week = c(1:10, 6:16*2),
  traj = rbind(matrix(seq(16, 19, length = 21), 50, 21, byrow = TRUE),
               matrix(seq(15.8, 18, length = 21), 30, 21, byrow = TRUE)) + rnorm(21*80, 0, 0.2)
)

To reach a slot

  • Everything in this section is rather dangerous
  • The access to the slots is made with the operator @
  • Use of @ should be avoided. The end-user should never have need to use it.
In [21]:
bmiVic@week
bmiVic@week <- c(1, 2, 4, 5)
bmiVic
  1. 1
  2. 3
  3. 4
  4. 5
An object of class "bmiTraj"
Slot "week":
[1] 1 2 4 5

Slot "traj":
     [,1] [,2] [,3] [,4]
[1,] 15.0 15.1 15.2 15.2
[2,] 16.0 15.9 16.0 16.4
[3,] 15.2   NA 15.3 15.3
[4,] 15.7 15.6 15.8 16.0

Default values

  • One can declare an object by giving it default values.
  • Add prototype argument.
In [22]:
setClass(
  Class = "bmiTrajNew",
  slots = list(
    week = "numeric",
    traj = "matrix"
  ),
  prototype = prototype(week = 1, traj = matrix(0))
)
  • Default initialization was something necessary in remote times
  • Today, it's no longer needed. If one does not initialize a field, R gives the object an adequate empty value.
  • Therefore, such initialization should be avoided.

Remove an object

One can remove a class using removeClass:

In [23]:
removeClass("bmiTrajNew")
new("bmiTrajNew")
TRUE
Error in getClass(Class, where = topenv(parent.frame())): “bmiTrajNew” is not a defined class
Traceback:

1. new("bmiTrajNew")
2. getClass(Class, where = topenv(parent.frame()))
3. stop(gettextf("%s is not a defined class", dQuote(Class)), domain = NA)
  • Removing the definition of a class does NOT remove the methods which are associated to it.
  • If you recreate it, all the old methods will still be valid.
  • To remove a class completely, it is necessary to remove the class then to remove all its methods.

To see an object

The following functions allow the program to see the contents or the structure of the objects

  • slotNames gives the name of the slots as a vector of type character.
  • getSlots gives the name of the slots and their type.
  • getClass gives the names of slots and their types, but also heirs and ancestors.
In [24]:
slotNames("bmiTraj")
  1. 'week'
  2. 'traj'
In [25]:
getSlots("bmiTraj")
week
'numeric'
traj
'matrix'
In [26]:
getClass("bmiTraj")
Class "bmiTraj" [in ".GlobalEnv"]

Slots:
                      
Name:     week    traj
Class: numeric  matrix

Methods

One of the interesting characteristics of object programming is to be able to define functions which will adapt their behavior to the object.

In [27]:
size <- rnorm(10, 1.70, 0.1)
weight <- rnorm(10, 70, 5)
group <- as.factor(rep(c("A", "B"), 5))
options(repr.plot.width=7, repr.plot.height=3)
par(mfrow = 1:2)
plot(size ~ weight)
plot(size ~ group)

"setMethod"

For that, one uses the function setMethod. It takes three arguments:

  • f is the name of the function which we are redefining. In our case, 'plot'
  • signature is the class of the object to which it applies.
  • definition is the function to be used.
In [28]:
setMethod(f = "plot", signature = "bmiTraj",
          definition = function(x, y, ...) {
            matplot(x@week, t(x@traj), xaxt="n", type="l", ylab="", xlab="", pch=1)
            axis(1, at=x@week)
          }
)
par(mfrow = 1:2); plot(bmiVic); plot(bmiAna)
'plot'
In [29]:
# Note: during the redefinition of a function, R imposes to use the same arguments as the function in question. 
# To know the arguments of the 'plot', one can use 'args'
args(plot)
args(print)
function (x, y, ...) 
NULL
function (x, ...) 
NULL

"show" and "print"

In [30]:
setMethod("print", "bmiTraj",
          function(x, ...) {

            cat("*** Class bmiTraj, method Print *** \n")
            cat("* Week = "); print(x@week)
            cat("* Traj = \n"); print(x@traj)
            cat("******* End Print (bmiTraj) ******* \n")
          }
)
print(bmiVic)
'print'
*** Class bmiTraj, method Print *** 
* Week = [1] 1 2 4 5
* Traj = 
     [,1] [,2] [,3] [,4]
[1,] 15.0 15.1 15.2 15.2
[2,] 16.0 15.9 16.0 16.4
[3,] 15.2   NA 15.3 15.3
[4,] 15.7 15.6 15.8 16.0
******* End Print (bmiTraj) ******* 
In [31]:
setMethod("show", "bmiTraj",
          function(object) {
            cat("*** Class bmiTraj, method show *** \n")
            cat("* Week = "); print(object@week)
            nrowShow <- min(10, nrow(object@traj))
            ncolShow <- min(10, ncol(object@traj))
            cat("* Traj (limited to a matrix 10x10) = \n")
            print(formatC(object@traj[1:nrowShow, 1:ncolShow]), quote = FALSE)
            cat("******* End Show (bmiTraj) ******* \n")
          }
)
bmiAna
'show'
*** Class bmiTraj, method show *** 
* Week =  [1]  1  2  3  4  5  6  7  8  9 10 12 14 16 18 20 22 24 26 28 30 32
* Traj (limited to a matrix 10x10) = 
      [,1]  [,2]  [,3]  [,4]  [,5]  [,6]  [,7]  [,8]  [,9]  [,10]
 [1,] 16.08 16.16 16.27 16.54 16.37 16.67 17.16 16.73 17.36 17.04
 [2,] 15.75 16.18 16.58 16.57 16.66 16.9  16.64 17.22 17.34 17.32
 [3,] 15.81 16.38 15.98 16.08 16.52 16.68 16.74 17.31 16.8  17.32
 [4,] 16.01 15.98 16.4  16.61 16.83 16.95 17.18 17.17 17.2  17.49
 [5,] 15.92 16.43 16.48 16.01 16.7  16.66 16.96 17.04 17.06 17.58
 [6,] 16.47 16.21 16.19 16.51 16.52 17.06 17.06 17.4  17.46 17.34
 [7,] 15.95 16.06 15.99 16.6  16.67 16.85 16.72 17    17.47 17.5 
 [8,] 15.65 15.83 16.57 16.36 16.41 16.81 17.25 16.9  17.11 17.27
 [9,] 15.92 16.19 16.53 16.49 16.27 16.62 16.88 17.49 17.13 17.18
[10,] 15.81 16.26 16.24 16.6  16.36 16.88 16.95 17.1  16.92 17.21
******* End Show (bmiTraj) ******* 
In [32]:
new("bmiTraj")
In [33]:
setMethod("show", "bmiTraj",
          function(object) {

            cat("*** Class bmiTraj, method show *** \n")
            cat("* Week = "); print(object@week)
            nrowShow <- min(10, nrow(object@traj))
            ncolShow <- min(10, ncol(object@traj))
            cat("* Traj (limited to a matrix 10x10) = \n")
            if(length(object@traj) != 0)
              print(formatC(object@traj[1:nrowShow, 1:ncolShow]), quote = FALSE)
            cat("******* End Show (bmiTraj) ******* \n")
          }
)
new("bmiTraj")
'show'
*** Class bmiTraj, method show *** 
* Week = numeric(0)
* Traj (limited to a matrix 10x10) = 
******* End Show (bmiTraj) ******* 

"setGeneric"

We now need to define a new method that is specific to bmiTraj. Therefore, it is necessary for us to declare it. This can be done by using the function setGeneric. This function requires two arguments:

  • name is the name of the method which we define.
  • def is an example of function which is used to define it.
In [34]:
setGeneric(name = "countMissing", def = function(object) standardGeneric("countMissing"))
setMethod(
  f = "countMissing",
  signature = "bmiTraj",
  definition = function(object) return(sum(is.na(object@traj)))
)
countMissing(bmiVic)
'countMissing'
'countMissing'
1

There is no control over the existence of a setGeneric: if a setGeneric existed, the new definition destroys the old one. A redefinition is often a mistake. One can "lock" the function using lockBinding.

To see the methods

Our class becomes more complex. It is time to take a little break and to admire our work.

In [35]:
showMethods(classes = "bmiTraj")
Function "asJSON":
 <not an S4 generic function>
Function: countMissing (package .GlobalEnv)
object="bmiTraj"

Function: initialize (package methods)
.Object="bmiTraj"
    (inherited from: .Object="ANY")

Function: plot (package graphics)
x="bmiTraj"

Function: print (package base)
x="bmiTraj"

Function: show (package methods)
object="bmiTraj"

In [36]:
# getMethod enables to see the definition (the contents of the body function) of a method for a given object
getMethod(f = "plot", signature = "bmiTraj")
An object of class “signature”
An object of class “signature”
structure(function (x, y, ...) 
{
    matplot(x@week, t(x@traj), xaxt = "n", type = "l", ylab = "", 
        xlab = "", pch = 1)
    axis(1, at = x@week)
}, target = structure("bmiTraj", .Names = "x", package = ".GlobalEnv", class = structure("signature", package = "methods")), defined = structure("bmiTraj", .Names = "x", package = ".GlobalEnv", class = structure("signature", package = "methods")), generic = structure("plot", package = "graphics"), class = structure("MethodDefinition", package = "methods"))
In [37]:
getMethod(f = "plot", signature = "bmiTraj")
An object of class “signature”
An object of class “signature”
structure(function (x, y, ...) 
{
    matplot(x@week, t(x@traj), xaxt = "n", type = "l", ylab = "", 
        xlab = "", pch = 1)
    axis(1, at = x@week)
}, target = structure("bmiTraj", .Names = "x", package = ".GlobalEnv", class = structure("signature", package = "methods")), defined = structure("bmiTraj", .Names = "x", package = ".GlobalEnv", class = structure("signature", package = "methods")), generic = structure("plot", package = "graphics"), class = structure("MethodDefinition", package = "methods"))
In [38]:
existsMethod(f = "plot", signature = "bmiTraj")
TRUE
In [39]:
existsMethod(f = "plot", signature = "bmiTrej")
FALSE

Construction

Constructions are some tools which enable to build a correct object:

  • methods of creation themselves
  • checking methods

The Inspector

The inspector is there to control that there is no internal inconsistency in the object. One gives it rules, then, at each object creation, it will check that the object follows the rules.

In [40]:
setClass(
  Class = "bmiTraj",
  slots = list(week = "numeric", traj = "matrix"),
  validity = function(object) {

    cat("--- bmiTraj: inspector --- \n")
    if(length(object@week) != ncol(object@traj))
      stop("[bmiTraj: validation] the number of weeks does not correspond to the number of columns of the matrix")
    return(TRUE)
  }
)
In [41]:
new(Class = "bmiTraj", week = 1:2, traj = matrix(1:2, ncol = 2))
--- bmiTraj: inspector --- 
*** Class bmiTraj, method show *** 
* Week = [1] 1 2
* Traj (limited to a matrix 10x10) = 
[1] 1 2
******* End Show (bmiTraj) ******* 
In [42]:
new(Class = "bmiTraj", week = 1:3, traj = matrix(1:2, ncol = 2))
--- bmiTraj: inspector --- 
Error in validityMethod(object): [bmiTraj: validation] the number of weeks does not correspond to the number of columns of the matrix
Traceback:

1. new(Class = "bmiTraj", week = 1:3, traj = matrix(1:2, ncol = 2))
2. initialize(value, ...)
3. initialize(value, ...)
4. validObject(.Object)
5. anyStrings(validityMethod(object))
6. validityMethod(object)
7. stop("[bmiTraj: validation] the number of weeks does not correspond to the number of columns of the matrix")   # at line 7-8 of file <text>
In [43]:
# The inspector will not be called after the creation of the object 
bmiPoppy <- new(Class = "bmiTraj", week = 1, traj = matrix(1))
(bmiPoppy@week <- 1:3)
--- bmiTraj: inspector --- 
  1. 1
  2. 2
  3. 3

The initializator

  • The initializator is a method that build an object and set all the slots to their value.
  • It is called at each object construction, i.e. with each use of the function 'new'.
In [44]:
setMethod(f = "initialize", signature = "bmiTraj",
          definition = function(.Object, week, traj) {
            cat("--- bmiTraj: initializator --- \n")
            rownames(traj) <- paste("I", 1:nrow(traj), sep='')
            .Object@traj <- traj
            .Object@week <- week
            return(.Object)
          }
)
new(Class = "bmiTraj", week = c(1, 2, 4, 5), traj = matrix(1:8, nrow = 2))
'initialize'
--- bmiTraj: initializator --- 
*** Class bmiTraj, method show *** 
* Week = [1] 1 2 4 5
* Traj (limited to a matrix 10x10) = 
   [,1] [,2] [,3] [,4]
I1 1    3    5    7   
I2 2    4    6    8   
******* End Show (bmiTraj) ******* 
In [45]:
new(Class = "bmiTraj", week = c(1, 2, 4), traj = matrix(1:8, nrow = 2))
--- bmiTraj: initializator --- 
*** Class bmiTraj, method show *** 
* Week = [1] 1 2 4
* Traj (limited to a matrix 10x10) = 
   [,1] [,2] [,3] [,4]
I1 1    3    5    7   
I2 2    4    6    8   
******* End Show (bmiTraj) ******* 
In [46]:
# To use an initializator and an inspector in the same object, it is thus necessary to call "manually" the inspector
setMethod(f = "initialize", signature = "bmiTraj",
          definition = function(.Object, week, traj) {
            cat("--- bmiTraj: initializator --- \n")
            if(!missing(traj)) {
              colnames(traj) <- paste("T", week, sep='')
              rownames(traj) <- paste("I", 1:nrow(traj), sep='')
              .Object@traj <- traj
              .Object@week <- week
              validObject(.Object)      # call of the inspector
            }
            return(.Object)
          }
)
new(Class = "bmiTraj", week = c(1, 2, 48), traj = matrix(1:8, nrow = 2))
'initialize'
--- bmiTraj: initializator --- 
Error in dimnames(x) <- dn: length of 'dimnames' [2] not equal to array extent
Traceback:

1. new(Class = "bmiTraj", week = c(1, 2, 48), traj = matrix(1:8, 
 .     nrow = 2))
2. initialize(value, ...)
3. initialize(value, ...)
4. .local(.Object, ...)
5. `colnames<-`(`*tmp*`, value = c("T1", "T2", "T48"))   # at line 6 of file <text>

Constructor for user

Aware that new is not a friendly function, the 'nicer' programmer would add user friendly constructors.

In [47]:
tr <- bmiTraj <- function(week, traj) {

  cat("----- bmiTraj: constructor ----- \n")
  new(Class = "bmiTraj", week = week, traj = traj)
}
bmiTraj(week = c(1, 2, 4), traj = matrix(1:6, ncol=3))
----- bmiTraj: constructor ----- 
--- bmiTraj: initializator --- 
--- bmiTraj: inspector --- 
*** Class bmiTraj, method show *** 
* Week = [1] 1 2 4
* Traj (limited to a matrix 10x10) = 
   T1 T2 T4
I1 1  3  5 
I2 2  4  6 
******* End Show (bmiTraj) ******* 
  • The interesting point is to be able to carry out some more sophisticated treatment.
  • For example, in a great number of cases the BMI is measured every week
In [48]:
tr <- bmiTraj <- function(week, traj) {

  if(missing(week)) week <- 1:ncol(traj)
  cat("----- bmiTraj: constructor ----- \n")
  new(Class = "bmiTraj", week = week, traj = traj)
}
bmiTraj(traj = matrix(1:8, ncol=4))
----- bmiTraj: constructor ----- 
--- bmiTraj: initializator --- 
--- bmiTraj: inspector --- 
*** Class bmiTraj, method show *** 
* Week = [1] 1 2 3 4
* Traj (limited to a matrix 10x10) = 
   T1 T2 T3 T4
I1 1  3  5  7 
I2 2  4  6  8 
******* End Show (bmiTraj) ******* 

Contrary to the initializator, one can define several constructors. Always under the assumption that the BMI increases by 0.1 every week, on can define regularBmiTraj:

In [49]:
regularBmiTraj <- function(nbWeek, BMIinit) {

  traj <- outer(BMIinit, 1:nbWeek, function(init, week) return(init+0.1*week))
  week <- 1:nbWeek
  return(new(Class = "bmiTraj", week = week, traj = traj))
}
regularBmiTraj(nbWeek = 3, BMIinit = 14:16)
--- bmiTraj: initializator --- 
--- bmiTraj: inspector --- 
*** Class bmiTraj, method show *** 
* Week = [1] 1 2 3
* Traj (limited to a matrix 10x10) = 
   T1   T2   T3  
I1 14.1 14.2 14.3
I2 15.1 15.2 15.3
I3 16.1 16.2 16.3
******* End Show (bmiTraj) ******* 

Note that the two constructors both call upon the initializator. Hence it's important to define a global initializator that can deal with all the cases.

Summary

During the construction of an object, there are three places where it is possible to carry out operations: in the construction's function, in the initializator and in the inspector. In order not to mix everything, it is thus preferable to specialize each one of these operators and to reserve each one precise tasks. Here is a possibility:

  • the construction function is to be called by the user. The function of construction always ends by 'new'.
  • the initializator is called by 'new'. If the initializator was not defined, R calls the default initializator which assigns the values to the slot, and then calls the inspector.
  • the inspector controls the internal conherence of the object. It cannot modify the values of the slot, it must just check that they follow the rules.

Another possibility, suggested by some (high level) programmers, is to NOT use the initializator at all and let the default initializator (more efficient) to be called. The construction function will do all the preparation.

Accessor

Using @ apart from a method is bad. However, it is necessary to be able to recover the values of slots. This is the role of accessors.

get

A getter is a method which returns the value of a slot.

  • e.g. When one uses names(data), R returns names of the columns of a data.frame
In [50]:
setGeneric("getWeek", function(object) standardGeneric("getWeek"))
setMethod("getWeek", "bmiTraj", function(object) return(object@week))
getWeek(bmiVic)
'getWeek'
'getWeek'
  1. 1
  2. 2
  3. 4
  4. 5
In [51]:
setGeneric("getTraj", function(object) standardGeneric("getTraj"))
setMethod("getTraj", "bmiTraj", function(object) return(object@traj))
getTraj(bmiVic)
'getTraj'
'getTraj'
15.015.115.215.2
16.015.916.016.4
15.2 NA15.315.3
15.715.615.816.0

set

A setter is a method which assigns a value to a slot.

  • e.g. when one uses names(data) <- c("A", "B"), R calls the function "names<-"
In [52]:
setGeneric("setWeek<-", function(object, value) standardGeneric("setWeek<-"))
setReplaceMethod(f = "setWeek", signature = "bmiTraj",
  definition = function(object, value) {
    object@week <- value
    return(object)
  }
)
getWeek(bmiVic)
'setWeek<-'
'setWeek<-'
  1. 1
  2. 2
  3. 4
  4. 5
In [53]:
setWeek(bmiVic) <- 1:3
getWeek(bmiVic)
  1. 1
  2. 2
  3. 3

The operator "["

  • Define the getters by using the operator [.
  • Define the setters by using the operator [<-.
In [54]:
setMethod(
  f = "[",
  signature = "bmiTraj",
  definition = function(x, i, j, drop) {

    if(i == "week") return(x@week)
    if(i == "traj") return(x@traj)
  }
)
bmiVic["week"]
bmiVic["traj"]
'['
  1. 1
  2. 2
  3. 3
15.015.115.215.2
16.015.916.016.4
15.2 NA15.315.3
15.715.615.816.0
In [55]:
setReplaceMethod(
  f = "[",
  signature = "bmiTraj",
  definition = function(x, i, j, value) {

    if(i == "week") x@week <- value
    if(i == "traj") x@traj <- value
    validObject(x)
    return(x)
  }
)
bmiVic["week"] <- 2:5
bmiVic["week"]
'[<-'
--- bmiTraj: inspector --- 
  1. 2
  2. 3
  3. 4
  4. 5

"[", "@" or "get/set"?

When shall we use 'get', @ or [?

  • @ should be used exclusively inside internal methods of the class.
  • Inside a class: one can use @ the entire time, or get the entire time.
  • For the object, the difference between getWeek(bmiVic) and bmiVic["week"] is subtle. It is more a question of taste.

PART III: To go further

Next steps include signatures, inheritance and some other advanced concepts.

Methods with several arguments

We made our first object. The following is more about interactions between objects.

The problem

It's an example. And we will only define the part we need.

In [56]:
setClass(
  Class = "ptGroup",
  slots = list(
    nbGroups = "numeric",
    group = "factor"
  )
)
setGeneric("getNbGroups", function(object) standardGeneric("getNbGroups"))
setMethod("getNbGroups", "ptGroup", function(object) return(object@nbGroups))
setGeneric("getGroup", function(object) standardGeneric("getGroup"))
setMethod("getGroup", "ptGroup", function(object) return(object@group))    
groupVic <- new(Class = "ptGroup", nbGroups = 2, group = factor(c("A", "B", "A", "B")))
groupAna <- new(Class = "ptGroup", nbGroups = 2, group = factor(rep(c("A", "B"), c(50, 30))))
'getNbGroups'
'getNbGroups'
Creating a new generic function for ‘getGroup’ in the global environment
'getGroup'
'getGroup'

Signature

  • Up to now, we use simple signatures made up of only one class.
  • In setMethod(f = "plot", signature = "bmiTraj", definition = function ...) the signature is simply "bmiTraj".
  • One can define a "signature vector" that will contain several classes.
  • when we call a method, R seeks the signature which corresponds the best to it and applies the corresponding function.
In [57]:
setGeneric("test", function(x, y, ...) standardGeneric("test"))
setMethod("test", "numeric", function(x, y, ...) cat("x is numeric =", x, "\n"))
'test'
'test'
In [58]:
test(3.17)
x is numeric = 3.17 
In [59]:
test("E")
Error in (function (classes, fdef, mtable) : unable to find an inherited method for function ‘test’ for signature ‘"character"’
Traceback:

1. test("E")
2. (function (classes, fdef, mtable) 
 . {
 .     methods <- .findInheritedMethods(classes, fdef, mtable)
 .     if (length(methods) == 1L) 
 .         return(methods[[1L]])
 .     else if (length(methods) == 0L) {
 .         cnames <- paste0("\"", vapply(classes, as.character, 
 .             ""), "\"", collapse = ", ")
 .         stop(gettextf("unable to find an inherited method for function %s for signature %s", 
 .             sQuote(fdef@generic), sQuote(cnames)), domain = NA)
 .     }
 .     else stop("Internal error in finding inherited methods; didn't return a unique method", 
 .         domain = NA)
 . })(list("character"), structure(function (x, y, ...) 
 . standardGeneric("test"), generic = structure("test", package = ".GlobalEnv"), package = ".GlobalEnv", group = list(), valueClass = character(0), signature = c("x", 
 . "y"), default = `\001NULL\001`, skeleton = (function (x, y, ...) 
 . stop("invalid call in method dispatch to 'test' (no default method)", 
 .     domain = NA))(x, y, ...), class = structure("standardGeneric", package = "methods")), 
 .     <environment>)   # at line 1 of file <text>
3. stop(gettextf("unable to find an inherited method for function %s for signature %s", 
 .     sQuote(fdef@generic), sQuote(cnames)), domain = NA)
In [60]:
setMethod("test", "character", function(x, y, ...) cat("x is character =", x, "\n"))
test("E")
'test'
x is character = E 
In [61]:
# More complicated, we wish that test shows a different behavior if one combines a numeric and a character.
setMethod(
  f = "test",
  signature = c(x = "numeric", y = "character"),
  definition = function(x, y, ...) {
    cat("more complicated: ")
    cat("x is numeric =", x, "AND y is a character =", y, "\n")
  }
)
test(3.2, "E")
'test'
more complicated: x is numeric = 3.2 AND y is a character = E 
In [62]:
test(3.2)
test("E")
x is numeric = 3.2 
x is character = E 

Back to our half real example. In the same way that we defined 'plot' for the signature bmiTraj, we now will define 'plot' for the signature c("bmiTraj", "ptGroup"):

In [63]:
options(repr.plot.width=7, repr.plot.height=3)
par(mfrow = c(1, 2))
plot(bmiVic); plot(bmiAna)
In [64]:
setMethod(
  f = "plot",
  signature = c(x = "bmiTraj", y = "ptGroup"),
  definition = function(x, y, ...) {
    matplot(x@week, t(x@traj[y@group == levels(y@group)[1], ]), ylim = range(x@traj, na.rm = TRUE),
            xaxt = "n", type = "l", ylab = "", xlab = "", col = 2)
    for(i in 2:y@nbGroups) {
      matlines(x@week, t(x@traj[y@group == levels(y@group)[i], ]), xaxt = "n", type = "l", col = i+1)
    }
    axis(1, at = x@week)
  }
)
par(mfrow = c(1, 2))
plot(bmiVic, groupVic)
plot(bmiAna, groupAna)
'plot'

Number of argument of a signature

  • A signature must count as many arguments as the methods to which it corresponds, neither more, nor less.
  • You can't add a second argument to print or a third argument to plot.

"ANY"

  • Reversely, the signature must count all the argument.
  • e.g. when we defined plot with only one argument, R added the second argument with type ANY.
  • To declare it explicitly, there is a special argument, the original class, the first cause: ANY.
In [65]:
showMethods(test)
Function: test (package .GlobalEnv)
x="character", y="ANY"
x="character", y="missing"
    (inherited from: x="character", y="ANY")
x="numeric", y="ANY"
x="numeric", y="character"
x="numeric", y="missing"
    (inherited from: x="numeric", y="ANY")

In [66]:
test(1, TRUE)
x is numeric = 1 

"missing"

  • It is also possible to define a method having a behavior if it has a single argument, another behavior if it has several.
  • For that, we need to use missing. missing is true if the argument is not present:
In [67]:
setMethod(
  f = "test",
  signature = c(x = "numeric", y = "missing"),
  definition = function(x, y, ...) cat("x is numeric =", x, "and y is 'missing' \n")
)
test(3.17)
test(3.17, "E")
test(3.17, TRUE)
'test'
x is numeric = 3.17 and y is 'missing' 
more complicated: x is numeric = 3.17 AND y is a character = E 
x is numeric = 3.17 

Inheritance

  • Inheritance is at least 50% of the power of the OOP.
  • We'll define bmiGroups.
  • To define the object, all the constructors, the setters, the getters and posting again, comapred to copy-and-paste, inheritance is more efficient and elegant

Inheritance tree

  • A class 'Son' can inherit a class 'Father' when 'Son' contains at least all the slots of 'Father' (maybe more).
  • Inheritance makes all the methods of 'Father' available for 'Son'.
  • More precisely, each time we use a method on an object of class 'Son', R will seek if this method exists. If it does not find it in the list of the specific methods 'Son', it will look for in the methods which 'Father' inherits. And so on.
  • The ultimate ancestor, the root of roots? ANY is the first class, the one from which all the others inherit.
  • Therefore, if a method is not found for the class 'Son', it will be sought in the class 'Father', then ANY.
  • ANY <- classFather <- classSon
In [68]:
#                         -----------------------
#                         |        ANY          |
#                         -----------------------
#                          ^                   ^
#                          |                   |
#                 -----------------     -----------------
#                 |  Father A     |     |  Father B     |
#                 -----------------     -----------------
#                  ^            ^             ^
#            -----------  -----------      -----------
#            |  Son A1 |  |  Son A2 |      |  Son B1 |
#            -----------  -----------      -----------
#                  ^
#            --------------
#            | Gd son A1a |
#            --------------
  • It is theoretically possible to inherit from several fathers. In this case, the graph is not a tree anymore.
  • Seems natural but dangerous.
  • e.g. if A inherits from B and C. When A is called upon a method not existing in A but in B and C at the same time, which method will be called for?

contains

We want to define bmiGroups as heiress of bmiTraj. For that, we have to declare the object adding the argument contains followed by the name of the father class.

In [69]:
setClass(
  Class = "bmiGroups",
  slots = list(groupList = "list"),
  contains = "bmiTraj"
)
bgLisa <- new("bmiGroups")
--- bmiTraj: initializator --- 

unclass

  • bmiGroups contains all the slots of bmiTraj plus its personal slot groupList.
  • For the moment, it is not possible to check it directly.
In [70]:
bgLisa
*** Class bmiTraj, method show *** 
* Week = numeric(0)
* Traj (limited to a matrix 10x10) = 
******* End Show (bmiTraj) ******* 

Why is it?

  • An object is seen using 'show', which has not been defined for bmiGroups. R calls show for bmiTraj instead.
  • It is then urgent to define a method show for bmiGroups.
  • Nevertheless, one can use unclass to remove the class of an object.
In [71]:
unclass(bgLisa)
<S4 Type Object>
attr(,"groupList")
list()
attr(,"week")
numeric(0)
attr(,"traj")
<0 x 0 matrix>

See the method by authorizing heritage

Heritage is a strenth, but it can lead to strange results.

In [72]:
groupVic2 <- new("ptGroup", nbGroups = 3, group = factor(c("A", "C", "C", "B")))
bgVic <- new(
  Class = "bmiGroups",
  week = c(1, 3, 4, 5),
  traj = bgVic@traj,
  groupList = list(groupVic, groupVic2)
)
Error in .local(.Object, ...): unused argument (groupList = list(<S4 object of class "ptGroup">, <S4 object of class "ptGroup">))
Traceback:

1. new(Class = "bmiGroups", week = c(1, 3, 4, 5), traj = bgVic@traj, 
 .     groupList = list(groupVic, groupVic2))
2. initialize(value, ...)
3. initialize(value, ...)
In [73]:
getMethod("initialize", "bmiGroups")
Error in getMethod("initialize", "bmiGroups"): no method found for function 'initialize' and signature bmiGroups
Traceback:

1. getMethod("initialize", "bmiGroups")
2. stop(gettextf("no method found for function '%s' and signature %s", 
 .     f, paste(signature, collapse = ", ")))
In [74]:
existsMethod("initialize", "bmiGroups")
FALSE
In [75]:
hasMethod("initialize", "bmiGroups")
TRUE
In [76]:
selectMethod("initialize", "bmiGroups")
An object of class “signature”
An object of class “signature”
structure(function (.Object, ...) 
{
    .local <- function (.Object, week, traj) 
    {
        cat("--- bmiTraj: initializator --- \n")
        if (!missing(traj)) {
            colnames(traj) <- paste("T", week, sep = "")
            rownames(traj) <- paste("I", 1:nrow(traj), sep = "")
            .Object@traj <- traj
            .Object@week <- week
            validObject(.Object)
        }
        return(.Object)
    }
    .local(.Object, ...)
}, target = structure("bmiGroups", class = structure("signature", package = "methods"), .Names = ".Object", package = "methods"), defined = structure("bmiTraj", .Names = ".Object", package = ".GlobalEnv", class = structure("signature", package = "methods")), generic = structure("initialize", package = "methods"), class = structure("MethodDefinition", package = "methods"))

bmiGroups used the initializator of bmiTraj and caused errors.

In [77]:
setMethod("initialize", "bmiGroups",
          function(.Object, week, traj, groupList) {
            cat("---- groupList: initializator ---- \n")
            if(!missing(traj)) {
              .Object@week <- week
              .Object@traj <- traj
              .Object@groupList <- groupList
            }
            return(.Object)
          }
)
bgVic <- new(
  Class = "bmiGroups",
  week = c(1, 3, 4, 5),
  traj = bmiVic@traj,
  groupList = list(groupVic, groupVic2)
)
'initialize'
---- groupList: initializator ---- 

"callNextMethod"

  • When R does not find a method, it has mechanism allowing to replace it by an inherited method.
  • It is possible to control this mechanism and to force a method to call the method inherited.
In [78]:
print(bgVic)
*** Class bmiTraj, method Print *** 
* Week = [1] 1 3 4 5
* Traj = 
     [,1] [,2] [,3] [,4]
[1,] 15.0 15.1 15.2 15.2
[2,] 16.0 15.9 16.0 16.4
[3,] 15.2   NA 15.3 15.3
[4,] 15.7 15.6 15.8 16.0
******* End Print (bmiTraj) ******* 
In [79]:
setMethod(
  f = "print",
  signature = "bmiGroups",
  definition = function(x, ...) {

    callNextMethod()    #### callNextMethod()
    cat("the object also contains", length(x@groupList), "groups. \n")
    cat("**** Fine of print (bmiGroups) **** \n")
    return(invisible())
  }
)
print(bgVic)
'print'
*** Class bmiTraj, method Print *** 
* Week = [1] 1 3 4 5
* Traj = 
     [,1] [,2] [,3] [,4]
[1,] 15.0 15.1 15.2 15.2
[2,] 16.0 15.9 16.0 16.4
[3,] 15.2   NA 15.3 15.3
[4,] 15.7 15.6 15.8 16.0
******* End Print (bmiTraj) ******* 
the object also contains 2 groups. 
**** Fine of print (bmiGroups) **** 

callNextMethod can either take explicit arguments, or no argument at all. In this case, the arguments which were given to the current method are completely shifted to the following method.

Whose method is the following one? Here lies all the difficulty and the ambiguity of callNextMethod.

Example: consider a class A that we program. Let us assume A inherits from class B, a class that somebody else programmed. Which is the method following initialize for A? It all depends. As A inherits, R seeks in the order:

  • initialize for B
  • Default initialize (the one for ANY). This method ends in a call for validObject.
  • validObject for A
  • validObject for B
  • Default validObject (the one for ANY).

The use of callNextMethod should be limited.

"is", "as" and "as<-"

When an object inherits from another, we can require that it adopts temporaily the behavior which its father would have. For that, it is possible to transform it into an object of the class of its father by using 'as'.

In [80]:
print(as(bgVic, "bmiGroups"))
*** Class bmiTraj, method Print *** 
* Week = [1] 1 3 4 5
* Traj = 
     [,1] [,2] [,3] [,4]
[1,] 15.0 15.1 15.2 15.2
[2,] 16.0 15.9 16.0 16.4
[3,] 15.2   NA 15.3 15.3
[4,] 15.7 15.6 15.8 16.0
******* End Print (bmiTraj) ******* 
the object also contains 2 groups. 
**** Fine of print (bmiGroups) **** 
In [81]:
# That will be useful to us in the definition of 'show for bmiGroups, no need to use callNextMethod
setMethod(
  f = "show",
  signature = "bmiGroups",
  definition = function(object) {

    show(as(object, "bmiTraj"))  ## Instead of callNextMethod, use 'as'
    lapply(object@groupList, show)
  }
)
# bgVic
'show'

We can check if an object is the heir of another by using "is".

In [82]:
is(bmiVic, "bmiGroups")
is(bgVic, "bmiTraj")
FALSE
TRUE

Lastly, "as<-" enables to modify the slot which an object inherits from its father. as(objectSon, "ClassFather") <- objectFather affects the contents of the slots that objectSon inherits from its father.

In [83]:
bgAna <- new("bmiGroups")
as(bgAna, "bmiTraj") <- bmiAna
bgAna
---- groupList: initializator ---- 
--- bmiTraj: initializator --- 
*** Class bmiTraj, method show *** 
* Week =  [1]  1  2  3  4  5  6  7  8  9 10 12 14 16 18 20 22 24 26 28 30 32
* Traj (limited to a matrix 10x10) = 
      [,1]  [,2]  [,3]  [,4]  [,5]  [,6]  [,7]  [,8]  [,9]  [,10]
 [1,] 16.08 16.16 16.27 16.54 16.37 16.67 17.16 16.73 17.36 17.04
 [2,] 15.75 16.18 16.58 16.57 16.66 16.9  16.64 17.22 17.34 17.32
 [3,] 15.81 16.38 15.98 16.08 16.52 16.68 16.74 17.31 16.8  17.32
 [4,] 16.01 15.98 16.4  16.61 16.83 16.95 17.18 17.17 17.2  17.49
 [5,] 15.92 16.43 16.48 16.01 16.7  16.66 16.96 17.04 17.06 17.58
 [6,] 16.47 16.21 16.19 16.51 16.52 17.06 17.06 17.4  17.46 17.34
 [7,] 15.95 16.06 15.99 16.6  16.67 16.85 16.72 17    17.47 17.5 
 [8,] 15.65 15.83 16.57 16.36 16.41 16.81 17.25 16.9  17.11 17.27
 [9,] 15.92 16.19 16.53 16.49 16.27 16.62 16.88 17.49 17.13 17.18
[10,] 15.81 16.26 16.24 16.6  16.36 16.88 16.95 17.1  16.92 17.21
******* End Show (bmiTraj) ******* 

"setIs"

In the case of a heritage, 'as' and 'is' are defined "naturally", as we have just seen before. It is also possible to specify them "manually".

For example, the class bmiGroups contains a list of ptGroup's. It does not inherit directly from ptGroup.

This can be done with the instruction setIs. setIs is a method which takes four arguments

  • class1 is the class of the initial object, the one which must be transformed.
  • class2 is the class into which the object must be transformed
  • coerce is the function used to transform class1 into class2. It uses 2 arguments, "from" correspond to class1 and "to" correspond to class2.
In [84]:
setIs(
  class1 = "bmiGroups",
  class2 = "ptGroup",
  coerce = function(from, to) {

    numberGroups <- sapply(from@groupList, getNbGroups)
    Smallest <- which.min(numberGroups)
    to <- new("ptGroup")
    to@nbGroups <- getNbGroups(from@groupList[[Smallest]])
    to@group <- getGroup(from@groupList[[Smallest]])
    return(to)
  }
)
is(bgVic, "ptGroup")
as(bgVic, "ptGroup")
Warning message in makeExtends(class1, class2, coerce, test, replace, by, classDef1 = classDef, :
“there is no automatic definition for 'as(object, "ptGroup") <- value' when object has class “bmiGroups” and no 'replace' argument was supplied; replacement will be an error”
TRUE
An object of class "ptGroup"
Slot "nbGroups":
[1] 2

Slot "group":
[1] A B A B
Levels: A B

A warning appears. R indicates that 'as<-' is not defined.

In our case, 'as<-' is the operator used to modify bmiGroups whereas it is regarded as "ptGroup".

In [85]:
setIs(class1 = "bmiGroups", class2 = "ptGroup",
      coerce = function(from, to) {
        numberGroups <- sapply(from@groupList, getNbGroups)
        Smallest <- which.min(numberGroups)
        to <- new("ptGroup")
        to@nbGroups <- getNbGroups(from@groupList[[Smallest]])
        to@group <- getGroup(from@groupList[[Smallest]])
        return(to)
      },
      replace = function(from, value) {
        numberGroups <- sapply(from@groupList, getNbGroups)
        Smallest <- which.min(numberGroups)
        from@groupList[[Smallest]] <- value
        return(from)
      }
)
as(bgVic, "ptGroup")
An object of class "ptGroup"
Slot "nbGroups":
[1] 2

Slot "group":
[1] A B A B
Levels: A B
In [86]:
as(bgVic, "ptGroup") <- groupVic2
# bgVic

Virtual classes

It happens that classes are close to each other without one being the extension of the other.

For example, we can conceive two types of ptGroup: ptGroup which "labels" individuals without judging them and those which evaluate individuals. The first type of ptGroup will not be ordered (same as ptGroup) and the second type of ptGroup will be ordered (e.g., low/medium/high). Slots of the second class will be nbGroups, an integer which indicates the number of modalities and part, an ordered variable.

One can use a virtual class instead of programming them twice.

In [87]:
setClass(
         Class = "ptGroupFather",
         slots = list(nbGroups = "numeric"),
         contains = "VIRTUAL"
)
new("ptGroupFather")
Error in new("ptGroupFather"): trying to generate an object from a virtual class ("ptGroupFather")
Traceback:

1. new("ptGroupFather")
In [88]:
setClass(Class = "ptGroupSimple",
         slots = list(part = "factor"),
         contains = "ptGroupFather"
)
setClass(Class = "ptGroupEval",
         slots = list(part = "ordered"),
         contains = "ptGroupFather"
)
In [89]:
setGeneric("nbMultTwo", function(object) {standardGeneric("nbMultTwo")})
setMethod("nbMultTwo", "ptGroupFather", function(object) {object@nbGroups <- object@nbGroups*2;   return(object)})
a <- new("ptGroupSimple", nbGroups = 3, part = factor(LETTERS[c(1, 2, 3, 2, 2, 1)]))
nbMultTwo(a)
# b <- new("ptGroupEval", nbGroups = 5, part = ordered(LETTERS[c(1, 5, 3, 4, 2, 4)]))
# nbMultTwo(b)
'nbMultTwo'
'nbMultTwo'
An object of class "ptGroupSimple"
Slot "part":
[1] A B C B B A
Levels: A B C

Slot "nbGroups":
[1] 6

Reference Classes (RC)

Reference classes (or RC for short) are the newest OO system in R. They were introduced in version 2.12. They are fundamentally different to S3 and S4 because:

  • RC methods belong to objects, not functions

  • RC objects are mutable: the usual R copy-on-modify semantics do not apply

These properties make RC objects behave more like objects do in most other programming languages, e.g., Python, Ruby, Java, and C#. Reference classes are implemented using R code: they are a special S4 class that wraps around an environment.

Defining classes and creating objects

Creating a new RC class is similar to creating a new S4 class, but you use setRefClass() instead of setClass().

In [91]:
student <- setRefClass("student")
student$new()
Reference class object of class "student"

setRefClass() also accepts a list of name-class pairs that define class fields (equivalent to S4 slots). Additional named arguments passed to new() will set initial values of the fields. You can get and set field values with $

In [100]:
student <- setRefClass("student",
  fields = list(Age = "numeric"))

Bob <- student$new(Age = 11)
cat("Bob is", Bob$Age, "year old. \n")

Bob$Age <- 12
cat("Bob is", Bob$Age, "year old. \n")
Bob is 11 year old. 
Bob is 12 year old. 

Note that RC objects are mutable, i.e., they have reference semantics, and are not copied-on-modify.

One can make a copy of an RC object by using copy() and make modifications.

In [102]:
Bob$Age <- 11
Mary <- Bob
Mary$Age <- 20
cat("Mary' is", Mary$Age, "year old. \n")
cat("Bob is", Bob$Age, "year old. \n")

Bob$Age <- 11
Mary <- Bob$copy()
Mary$Age <- 20
cat("Mary' is", Mary$Age, "year old. \n")
cat("Bob is", Bob$Age, "year old. \n")
Mary' is 20 year old. 
Bob is 20 year old. 
Mary' is 20 year old. 
Bob is 11 year old. 

Methods should be defined within the class definition (different from S4). And values can be reached and modified using <<-.

In [105]:
student <- setRefClass("student",
  fields = list(Age = "numeric"),
  methods = list(
    grow = function(x = 1) {
      Age <<- Age + x
    },
    setAge = function(x) {
      Age <<- x
    }
  )
)
Bob <- student$new(Age = 11)
Bob$grow()
cat("Bob is", Bob$Age, "year old. \n")
Bob$setAge(11)
cat("Bob is", Bob$Age, "year old. \n")
Bob is 12 year old. 
Bob is 11 year old. 

One can use contains to inherit from an RC.

In [107]:
studentPlus <- setRefClass("studentPlus", 
                          contains = "student",
                          methods = list(
                              setAge = function(x) {
                                  if(x < 0) stop("Age can't be under 0. ")
                                  Age <<- x
                              }
                          ))

Bob <- studentPlus$new(Age = 11)
Bob$grow(2)
cat("Bob is", Bob$Age, "year old. \n")

Bob$setAge(-1)
Bob is 13 year old. 
Error in Bob$setAge(-1): Age can't be under 0. 
Traceback:

1. Bob$setAge(-1)
2. stop("Age can't be under 0. ")   # at line 5 of file <text>
In [ ]: