Object oriented programming (OOP) is a widespread philosophy: it's the cornerstone of popular languages like Java, Python and C++.
The concepts of class and method are central to any OOP system:
A class defines an object's properties and how it relates to other objects. Every object is an instance of a class.
A method is a function associated with a particular type of object
While R supports a mixture of OOP and functional programming, the latter is relatively more important than OOP in everyday R usage:
We solve complex problems by decomposing them into simple functions rather than objects!
Nonetheless, being familiar with R's widely used OOP systems is important for
a more general understanding of the language
being able to understand and expand on object oriented code from other authors
R's most important OOP systems are S3, S4, R6 and RC
S3 is R's oldest OO system. It's minimal but elegant.
S4 is similar but more formal than S3
R6 builds on environments
RC implements encapsulated object orientation
Engaging with OOP in R is challenging due to
differences to the OOP systems used by other languages
disagreement over the realtive importance of the available OOP systems
We will elaborate key principles in OOP with R, thereby focusing on S3
sloop
PackageWe 'sail the seas of OOP' using the sloop
package. It provides helper functions which facilitate the handling of OOP objects.
## install.packages('sloop')library('sloop')
otype(base::abs)
## [1] "base"
otype(3:1)
## [1] "base"
otype(lm(area ~ poptotal, data = ggplot2::midwest))
## [1] "S3"
Everything that exists in R is an object. — John Chambers
Thus far we have used the term 'object' somewhat sloppily — not every R object is object-oriented:
Source: Wickham (2019)
is.object()
and sloop::otype()
are useful for identifying objects in the wild.
# base objectis.object(1L)
## [1] FALSE
otype(1L)
## [1] "base"
# OO objectis.object(ggplot2::diamonds)
## [1] TRUE
otype(ggplot2::diamonds)
## [1] "S3"
Only OO objects have a class
attribute.
attr(1L, "class")
## NULL
attr(ggplot2::diamonds, "class")
## [1] "tbl_df" "tbl" "data.frame"
There are alternate functions for checking the class
argument
s3_class(ggplot2::diamonds)
## [1] "tbl_df" "tbl" "data.frame"
class(ggplot2::diamonds)
## [1] "tbl_df" "tbl" "data.frame"
Be careful with class()
class(1L)
## [1] "integer"
class()
can be misleading when used with base objects which is why we use sloop::s3_class()
sloop::s3_class()
works safely with base, S3 and S4 objects.
(returns the implicit class used by the respective system)
Every R object has a base type of which there are 25.
Important types are NULL
, logical
, integer
, double
, complex
, character
, list
and closure
typeof(1L)
## [1] "integer"
typeof(ggplot2::diamonds)
## [1] "list"
typeof(lm)
## [1] "closure"
numeric
typeThe definition of the numeric
type is inconsistent:
numeric
is sometimes used as an alias for the double
typenumeric
is an alias for integer
and double
types in S3/S4base::is.numeric()
checks if the object behaves like a numberLet's run the following expressions and comment on the results.
is.numeric(3.14159)is.numeric(1L)typeof(factor('x'))is.numeric(factor('x'))
What is S3?
C++ and Java use message-passing style where objects encapsulate data and functions that may modify the data.
A typical function call looks like object.method(argument)
.
S3 implements a functional style where the associated functions (called generics) live outside the object. A generic decides which method to call based on the object passed.
A typical function call looks like generic(object, argument)
.
Why discussing S3?
S3 is simple but flexible and will likely be sufficient for your OO code projects.
It's used in base
and stats
and is the most commonly used system on CRAN.
This sections provides you with a working knowledge of the system and its limitations. Being familiar with S3 is a good starting point for engaging with the other systems, if necessary for your work.
Every S3 object has a base type and at least one class.
f <- structure(1:3, class = "factor", levels = c("a", "b", "c"))
We check S3 membership, base type and the class of f
.
otype(f)
## [1] "S3"
typeof(f)
## [1] "integer"
class(f)
## [1] "factor"
Check that class
is simply an attribute
of f
using attribute()
.
A generic function (generic) is an interface. It behaves dependent on the class of the passed S3 object.
This concept is called polymorphism (many shapes).
The behavior when a S3 object is passed to a generic may be very different from the underlying base type: class-specific methods may do weird stuff!
The process of looking for a class-specific implementation (method) upon a generic function call is called method dispatch
print()
Executing f
calls a generic (print
) with specific behavior for objects of class factor
.
ftype(print)
## [1] "S3" "generic"
print(f) # Or simply `f`
## [1] a b c## Levels: a b c
# Integer behaviourprint(unclass(f))
## [1] 1 2 3## attr(,"levels")## [1] "a" "b" "c"
unclass(f) + unclass(f)
S3 methods follow the name convention generic.class()
We may use s3_dispatch()
to investigate the process of method dispatch for an S3 object
s3_dispatch(print(f))
## => print.factor## * print.default
=>
indicates the method used*
idicates that the method is defined (but not used)
Methods are sometimes exported from the package namespace (like print.factor()
) but generally should not be used directly
ftype(print.factor) # check if method
## [1] "S3" "method"
Use s3_get_method()
to see the source code of a method which is not exported.
lm.print()
mod <- lm(price ~ carat, data = ggplot2::diamonds)typeof(mod)
## [1] "list"
class(mod)
## [1] "lm"
lm.print()
— ctd.
# Inspect hidden print method for lm objectss3_get_method(print.lm)
## function (x, digits = max(3L, getOption("digits") - 3L), ...) ## {## cat("\nCall:\n", paste(deparse(x$call), sep = "\n", collapse = "\n"), ## "\n\n", sep = "")## if (length(coef(x))) {## cat("Coefficients:\n")## print.default(format(coef(x), digits = digits), print.gap = 2L, ## quote = FALSE)## }## else cat("No coefficients\n")## cat("\n")## invisible(x)## }## <bytecode: 0x7fbefa268660>## <environment: namespace:stats>
Describe the difference between t.test()
and t.data.frame()
. When is each function called?
What class of object does the following code return? What base type is it built on? What attributes does it use?
x <- ecdf(rpois(100, 10))x
What class of object does the following code return? What base type is it built on? What attributes does it use?
x <- table(rpois(100, 5))x
Part of S3's simplicity is due to lack of formal definition what a class is. For the largest part, this is up to the programmer.
We have already seen how to generate an OO object using a base type and a class with structure()
.
We may use class()
to change the class of existing objects. There are no checks if objects of the same class share the same structure!
class(mod) <- "data.frame"mod
## [1] coefficients residuals effects rank fitted.values## [6] assign qr df.residual xlevels call ## [11] terms model ## <0 rows> (or 0-length row.names)
We recommended to follow the following conventions when creating your own classes:
Avoid .
in class names to prevent confusion with generics
Provide a constructor function, new_classname()
, which generates objects with the desired structure
Use a validator function, validate_classname()
to check that objects of your class have correct values
"Sharing is caring": provide a user-friendly helper, classname()
, which makes it easy for others to generate objects of your class
We now demonstrate how to implement a simple S3 class for prime numbers.
prime
classLet's start with a constructor function which accepts integer input and returns an object with classes 'data frame' and 'prime'.
new_prime <- function(x = integer()) { stopifnot(is.integer(x)) x <- list(x) class(x) <- 'prime' x}
prime
classThe validator function should check whether the values of a prime
object are indeed prime numbers.
Note that the integer z>2 is a prime number if zmody>0∀y∈[2,3,...,z−1].
validate_prime <- function(x = list()) { sapply( unique(unlist(x)), function(z) { if(!all(z==2 || z %% 2:(z-1) > 0)) { stop('Input contains non-prime number(s)!', call. = F) } } ) x}
(This code is not efficient. Can you come up with a better implementation?)
prime
classAt last we provide a user-friendly helper function which generates and validates prime
objects.
prime <- function(x = integer()) { validate_prime(new_prime(x))}
(x <- prime(c(3L, 5L, 7L, 11L)))
## [[1]]## [1] 3 5 7 11## ## attr(,"class")## [1] "prime"
## [1] "prime"
Many prominent R functions are generics, e.g. mean()
. Method dispatch is done by UseMethod()
.
mean
## function (x, ...) ## UseMethod("mean")## <bytecode: 0x7fbef74f1720>## <environment: namespace:base>
Let's see the dispatch for mean(x)
if x
has class Date
:
x <- Sys.Dates3_dispatch(mean(x))
## mean.function## => mean.default
Note that the pseudo-class default
for standard fallback could be used for Date
objects but there is a specific method mean.Date()
which is called instead
Use s3_methods_generic()
to see all available methods of a generic
s3_methods_generic('mean')
## # A tibble: 6 x 4## generic class visible source ## <chr> <chr> <lgl> <chr> ## 1 mean Date TRUE base ## 2 mean default TRUE base ## 3 mean difftime TRUE base ## 4 mean POSIXct TRUE base ## 5 mean POSIXlt TRUE base ## 6 mean quosure FALSE registered S3method
s3_method_class()
provides all registered methods for a given class.
head( s3_methods_class('Date'), 10)
## # A tibble: 10 x 4## generic class visible source## <chr> <chr> <lgl> <chr> ## 1 - Date TRUE base ## 2 [ Date TRUE base ## 3 [[ Date TRUE base ## 4 [<- Date TRUE base ## 5 + Date TRUE base ## 6 as.character Date TRUE base ## 7 as.data.frame Date TRUE base ## 8 as.list Date TRUE base ## 9 as.POSIXct Date TRUE base ## 10 as.POSIXlt Date TRUE base
Writing your own generic is straightforward
my_generic <- function(x) { UseMethod('my_generic')}
UseMethod()
dispatches based on x
by default. Dispatch based on a second argument is optional.
Let's define methods for default behavior and my_class
my_generic.default <- function(x) { x}my_generic.my_class <- function(x) { cat("Output for class 'my_class':\n", x)}
comment on rules for creating methods (CH 13.4.3 in Advanced R)
We next check that method dispatch works as desired:
y <- structure(x<-1, class = 'my_class')s3_dispatch(my_generic(x))
## my_generic.double## my_generic.numeric## => my_generic.default
s3_dispatch(my_generic(y))
## => my_generic.my_class## * my_generic.default
We next check that method dispatch works as desired:
y <- structure(x<-1, class = 'my_class')my_generic(x)
## [1] 1
my_generic(y)
## Output for class 'my_class':## 1
prime
Objectsprime
ObjectsLet's write a method for objects of class prime
for the generic plot()
. The method should
take objects of class prime and all additional arguments which can be passed to plot()
be able to visualise a prime number p in Cartesian and polar coordinates
Source: http://www.maths.usyd.edu.au/
prime
Objects
plot.prime <- function(x = list(), coord, ...) { x <- unlist(x) if(missing(coord)) { d <- data.frame(x = x, y = x) } else if(coord == "polar") { d <- data.frame(x = cos(x * 180/pi) * x, y = sin(x * 180/pi) * x) } plot(d, ...)}
Note that we may pass additional arguments to plot()
using ...
prime
Objects
# Subset of all prime numbers in 2,...,1e4x <- 2:1e4x <- x[sapply(unique(x), function(z) all(z %% 2:(z-1) > 0))]# Assign 'prime' classx <- prime(x)
Let's check the method dispatch for plot(x)
s3_dispatch(plot(x))
## => plot.prime## * plot.default
prime
Objects
plot(x, coord = "polar", pch = 19, cex = 0.5, col = "red")
Read the source code for t()
and t.test()
and confirm that t.test()
is an S3 generic and not an S3 method. What happens if you create an object with class test
and call t()
with it? Why?
What generics does the table
class have methods for?
Implement a summary()
method for objects of class prime
. Use the following strategy:
A summary()
method should do some computations and return an object of class summary_print
but not print the results
A separate print()
method for summary_prime
objects should output the results in an appealing format
In OOP, the concept of Inheritance allows us to derive a new class (child) from an existing one (parent).
Using inheritance we don't need to start from scratch: the parent may share data, methods etc. with the child
Inheritance in S3 boils down to a proper assignment of classes (and proper definitions of corresponding methods)
Note that we may assign multiple classes to an S3 object
class(x) <- c('A', 'B')class(x) <- c('B', 'A')
Method dispatch always starts with the first element of the class vector and proceeds with subsequent classes if a method is not found.
This clarifies the informality of the S3 system: there is no formal definition of how classes relate to each other.
# New genericg <- function(x) UseMethod("g", x)# Methods for classes 'A' and 'B'g.A <- function(x) "A"; g.B <- function(x) "B"# Two objects with different inheritance hierachiesab <- structure(1, class = c("A", "B")); ba <- structure(1, class = c("B", "A"))g(ab)
## [1] "A"
g(ba)
## [1] "B"
It's possible to force delegation to the method of the subsequent class (if defined) using NextMethod()
. This provides a simple inheritance mechanism:
g.C <- function(x) NextMethod()ca <- structure(1, class = c("C", "A"))cb <- structure(1, class = c("C", "B"))g(ca)
## [1] "A"
g(cb)
## [1] "B"
Notice that inheritance depends on how we define the class vector:
ca
, class C
inherits its g()
method from class A
'cb
, class C
inherits its g()
method from class B
'We may use these mechanics to implement 2 (child) classes which inherit (parts of) their print()
method from the prime
class.
print()
method
print.prime <- function(x) { x[[1]]}
print.mersenne <- function(x) { cat('Mersenne primes:', NextMethod(x))}print.fermat <- function(x) { cat('Fermat primes:', NextMethod(x))}
print()
method — ctd.
x <- prime(c(3L, 7L, 31L))class(x) <- c('mersenne', class(x))y <- prime(c(3L, 5L, 17L, 257L))class(y) <- c('fermat', class(y))x
## Mersenne primes: 3 7 31
y
## Fermat primes: 3 5 17 257
print()
method — ctd.
s3_dispatch(print(x))
## => print.mersenne## -> print.prime## * print.default
s3_dispatch(print(y))
## => print.fermat## -> print.prime## * print.default
->
indicates the method the call to the original method has been delegated to.
Adjust the constructor function new_prime()
such that it allows for subclasses. Then implement a constructor function new_fermat()
for objects with subclass fermat
.
What happens if you subset x
(generated as below) using [[
?
x <- prime(c(2L, 3L, 5L))
Implement a [[
method which preserves the prime
class.
Object oriented programming (OOP) is a widespread philosophy: it's the cornerstone of popular languages like Java, Python and C++.
The concepts of class and method are central to any OOP system:
A class defines an object's properties and how it relates to other objects. Every object is an instance of a class.
A method is a function associated with a particular type of object
While R supports a mixture of OOP and functional programming, the latter is relatively more important than OOP in everyday R usage:
We solve complex problems by decomposing them into simple functions rather than objects!
Nonetheless, being familiar with R's widely used OOP systems is important for
a more general understanding of the language
being able to understand and expand on object oriented code from other authors
Keyboard shortcuts
↑, ←, Pg Up, k | Go to previous slide |
↓, →, Pg Dn, Space, j | Go to next slide |
Home | Go to first slide |
End | Go to last slide |
Number + Return | Go to specific slide |
b / m / f | Toggle blackout / mirrored / fullscreen mode |
c | Clone slideshow |
p | Toggle presenter mode |
t | Restart the presentation timer |
?, h | Toggle this help |
Esc | Back to slideshow |