R Programming
March 18, 2024
This material is largely based on Chapters 12 and 13 of Advanced R, 2nd edition, by Hadley Wickham.
The book is freely available online: https://adv-r.hadley.nz.
It is shared under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
With OOP, a developer can consider a function’s interface separately from its implementation.
This makes it possible to use the same function for different types of input.
These are called generic functions.
OO systems call the type of an object its class.
An implementation for a specific class is called a method.
The class defines the fields, the data possessed by every instance of that class.
Tip
Roughly speaking, a class defines what an object is and methods define what an object can do.
Classes are organised in a hierarchy, so that if a method does not exist for one class, its parent’s method is used.
The child is said to inherit behaviour.
The process of finding the correct method given a class is called method dispatch.
Generic functions provide a unified interface to methods for objects of a particular class, e.g.
Adelie Chinstrap Gentoo
152 68 124
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
172 190 197 201 213 231 2
Here, we use the same function, summary()
, on objects of classes factor
and integer
and get different output for each.
summary()
could contain several if-else statements, but
There are 3 main OOP systems in use:
A new OOP system, S7, is in development as a successor to S3 and S4.
The sloop package provides tools to help you interactively explore and understand object oriented programming in R, particularly with S3.
In R, we can distinguish between base objects and OO objects.
A base object:
An OO object:
Techincally, the difference between base and OO objects is that OO objects have a class attribute:
Only OO objects have a class attribute, but every object has a base type.
There are 25 different base types, e.g.
Jenny Bryan’s talk on debugging:
https://posit.co/resources/videos/object-of-type-closure-is-not-subsettable/
An S3 object has a "class"
attribute:
With unclass()
we obtain the underlying object, its base type, here an integer vector
The generic is the middleman: its job is to define the interface (i.e. the arguments) then find the right implementation for the job. The implementation for a specific class is called a method, and the generic finds that method by performing method dispatch.
Hadley Wickham, Advanced R (2e)
S3 methods are functions with a special naming scheme, generic.class()
. For example, the factor
method for the print()
generic is called print.factor()
.
You should never call the method directly, but instead rely on the generic to find it for you.
Tip
This is why it is not considered best practice to use .
when naming your own functions.
Warning
Lots of important R functions that are not methods do have .
in the title – these are from before S3.
To make an object an instance of a class, you simply set the class attribute.
(S3 has no formal definition of a class).
stucture()
You can use structure()
to define an S3 object with a class attribute:
$pi
[1] 3.14
$dp
[1] 2
attr(,"class")
[1] "pi_trunc"
Potentially further attributes can be added at the same time, but typically we would use a list to return all the required values.
class()
Alternatively, we can add a class attribute using the class()
helper function:
S3 has no checks for correctness, so we can change the class of objects.
This is a bad idea!
[1] "lm"
Error in as.POSIXlt(.Internal(Date2POSIXlt(x, tz)), tz = tz): 'list' object cannot be coerced to type 'double'
R doesn’t stop you from shooting yourself in the foot, but as long as you don’t aim the gun at your toes and pull the trigger, you won’t have a problem.
All objects of the same class should have the same structure, i.e. same base type and same attributes.
Recommend that you create:
new_myclass()
, that efficiently creates objects with the correct structurevalidate_myclass()
that performs more computationally expensive checks to ensure the object has correct valuesmyclass()
, that provides a convenient way for others to create objects of your class.See https://adv-r.hadley.nz/s3.html#s3-classes for more details.
S3 generic functions are simple wrappers to UseMethod()
useMethod()
The UseMethod()
function takes care of method dispatch: selecting the S3 method according to the class of the object passed as the first argument.
[1] "factor"
[1] Adelie Adelie Adelie
Levels: Adelie Chinstrap Gentoo
Here print()
dispatches to the method print.factor()
.
s3_dispatch()
UseMethod()
creates a vector of method names then looks for each potential method in turn. We can see this with sloop::s3_dispatch()
:
=>
indicates the method that is called here.*
indicated a method that is defined, but not called.default
default
is a special pseudo-class that provides a fallback whenever a class-specific method is not available.
print.pi_trunc
is not defined.
An S3 object can have more than one class e.g.
UseMethod()
works along the vector of classes (from the first class to the last), looks for a method for each class and dispatches to the first method it finds.
If no methods are defined for any of class, the default is used , e.g. print.default()
.
If there is no default, an error is thrown.
See the methods for a given S3 class:
[1] anova coef confint deviance df.residual fitted
[7] formula logLik nobs predict print profile
[13] residuals summary vcov weights
see '?methods' for accessing help and source code
# A tibble: 6 × 4
generic class visible source
<chr> <chr> <lgl> <chr>
1 anova nls FALSE registered S3method
2 coef nls FALSE registered S3method
3 confint nls FALSE registered S3method
4 deviance nls FALSE registered S3method
5 df.residual nls FALSE registered S3method
6 fitted nls FALSE registered S3method
See the methods for a given generic function:
[1] coef.aov* coef.Arima* coef.default* coef.listof* coef.maov*
[6] coef.nls*
see '?methods' for accessing help and source code
Asterisked methods are not exported.
# A tibble: 6 × 4
generic class visible source
<chr> <chr> <lgl> <chr>
1 coef aov FALSE registered S3method
2 coef Arima FALSE registered S3method
3 coef default FALSE registered S3method
4 coef listof FALSE registered S3method
5 coef maov FALSE registered S3method
6 coef nls FALSE registered S3method
S3 methods need not be in the same package as the generic.
Find an unexported method with getS3method()
or sloop::s3_get_method()
function (object, complete = TRUE, ...)
{
cf <- object$coefficients
if (complete)
cf
else cf[!is.na(cf)]
}
<bytecode: 0x11158e8d8>
<environment: namespace:stats>
The arguments of a new method should be a superset of the arguments of the generic
New methods have the name format generic.class
:
S3 classes can share behaviour through a mechanism called inheritance. Inheritance is powered by three ideas.
The class can be a character vector
If a method is not found for the class in the first element of the vector, R looks for a method in the second class (and so on)
A method can delegate work by calling NextMethod()
.
The class of an S3 object can be a vector of classes:
We say fit
is a "glm"
object that inherits from class "lm"
.
glm
is a subclass of lm
, because it always appears before it in the class vector.
lm
is a superclass of glm
.
inherits()
The inherits()
function can be used to test if an object inherits from a given class:
Create a function to fit an ordinary least squares model given a response y
and an explanatory variable x
, that returns an object of a new class "ols"
, that inherits from "lm"
.
Define a print method for your function that it works as follows:
Note: I have set options(digits = 4)
to limit the number of digits printed by default throughout this presentation (default is 7).
NextMethod()
Hard to understand, so here’s a concrete example for the common use case: [
.
We want this to be secret! . . .
The default [
method doesn’t preserve the class.
So, need to defined a [.secret
method.
But the following doesn’t work:
It gets stuck in infinite loop.
We need some way of calling the underlying [
code, i.e. the implementation that would get called if we didn’t have a [.secret
method.
i.e. we’re defining [.secret
but we still want to access the internal [
method (so we don’t get stuck in a loop) as if [.secret
wasn’t defined.
NextMethod()
The =>
indicates that [.secret
is called, but that NextMethod()
delegates work to the underlying internal [
method, as shown by ->
.
NextMethod()
examplefunction (x)
{
x <- as.matrix(x)
NextMethod("t")
}
<bytecode: 0x1172a2270>
<environment: namespace:base>
We can explicitly call the next method that would be called by UseMethod()
to reuse code whilst customising as required.
As we’ve seen, is.object()
or sloop::otype()
can be used to find out if an object has a class (S3/S4/R6)
An object that does not have an explicit class has an implicit class that will be used for S3 method dispatch.
The implicit class can be found with .class2()
, or sloop::s3_class()
NULL
[1] "matrix" "array" "integer" "numeric"
[1] "matrix" "integer" "numeric"
The class()
of an object does not uniquely determine its dispatch:
We can take advantage of existing S3 methods by returning an object of a existing S3 class or an implicit class, using attributes to add custom information
[1] "matrix" "array"
V1 V2
Min. :-2 Min. :-4
1st Qu.:-1 1st Qu.:-2
Median : 0 Median : 0
Mean : 0 Mean : 0
3rd Qu.: 1 3rd Qu.: 2
Max. : 2 Max. : 4
This can avoid the need to define new classes and methods, in simple cases.
In scale.default()
the attribute "scaled:center"
is added to the x
argument, so essentially, center_x
is a matrix with extra information (in this case, the colMeans of the original columns).
Write a summary method for your ols
class that uses NextMethod()
to compute the usual lm
summary, but return an object of class "summary.ols"
.
Write a print method for the "summary.ols"
which works as follows:
S4 provides a formal approach to OOP. Its implementation is much stricter than S3.
S4 has slots, a named component of the object accessed with @
.
S4 methods:
Main reference for this session, goes a bit further (including R6): Wickham, H, Advanced R (2nd edn), Object-oriented programming section, https://adv-r.hadley.nz/oo.html
Fun example creating Turtle
and TurtleWithPen
classes to create simple graphics by moving the turtle: https://stuartlee.org/2019/07/09/s4-short-guide/
Licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0).