+ - 0:00:00
Notes for current slide
Notes for next slide

Advanced R for Econometricians

Object Oriented Programming in R

Martin Arnold

1 / 44

Introduction

  • 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

2 / 44

Introduction

  • 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

3 / 44

The sloop Package

We 'sail the seas of OOP' using the sloop package. It provides helper functions which facilitate the handling of OOP objects.

Copy to clipboard
## install.packages('sloop')
library('sloop')
Copy to clipboard
otype(base::abs)
## [1] "base"
Copy to clipboard
otype(3:1)
## [1] "base"
Copy to clipboard
otype(lm(area ~ poptotal, data = ggplot2::midwest))
## [1] "S3"
4 / 44

Base Types vs OO Objects


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)

5 / 44

Base Types vs OO Objects

is.object() and sloop::otype() are useful for identifying objects in the wild.

Copy to clipboard
# base object
is.object(1L)
## [1] FALSE
Copy to clipboard
otype(1L)
## [1] "base"
Copy to clipboard
# OO object
is.object(ggplot2::diamonds)
## [1] TRUE
Copy to clipboard
otype(ggplot2::diamonds)
## [1] "S3"
6 / 44

Base Types vs OO Objects

Only OO objects have a class attribute.

Copy to clipboard
attr(1L, "class")
## NULL
Copy to clipboard
attr(ggplot2::diamonds, "class")
## [1] "tbl_df" "tbl" "data.frame"
7 / 44

Base Types vs OO Objects

  • There are alternate functions for checking the class argument

    Copy to clipboard
    s3_class(ggplot2::diamonds)
    ## [1] "tbl_df" "tbl" "data.frame"
    Copy to clipboard
    class(ggplot2::diamonds)
    ## [1] "tbl_df" "tbl" "data.frame"
  • Be careful with class()

    Copy to clipboard
    class(1L)
    ## [1] "integer"
8 / 44
  • 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)

Base Types

Every R object has a base type of which there are 25.

Important types are NULL, logical, integer, double , complex, character, list and closure

Copy to clipboard
typeof(1L)
## [1] "integer"
Copy to clipboard
typeof(ggplot2::diamonds)
## [1] "list"
Copy to clipboard
typeof(lm)
## [1] "closure"
9 / 44

Base Types — numeric type

The definition of the numeric type is inconsistent:

  • numeric is sometimes used as an alias for the double type
  • numeric is an alias for integer and double types in S3/S4
  • base::is.numeric() checks if the object behaves like a number

Let's run the following expressions and comment on the results.

Copy to clipboard
is.numeric(3.14159)
is.numeric(1L)
typeof(factor('x'))
is.numeric(factor('x'))
10 / 44

S3 — Basics

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.

11 / 44

S3 — Basics

Every S3 object has a base type and at least one class.

Example: class and type of factor object

Copy to clipboard
f <- structure(1:3, class = "factor", levels = c("a", "b", "c"))

We check S3 membership, base type and the class of f.

Copy to clipboard
otype(f)
## [1] "S3"
Copy to clipboard
typeof(f)
## [1] "integer"
Copy to clipboard
class(f)
## [1] "factor"
12 / 44

Check that class is simply an attribute of f using attribute().

S3 — Generics

  • 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

13 / 44

S3 — Generics

Example: print()

Executing f calls a generic (print) with specific behavior for objects of class factor.

Copy to clipboard
ftype(print)
## [1] "S3" "generic"
Copy to clipboard
print(f) # Or simply `f`
## [1] a b c
## Levels: a b c
Copy to clipboard
# Integer behaviour
print(unclass(f))
## [1] 1 2 3
## attr(,"levels")
## [1] "a" "b" "c"
14 / 44

unclass(f) + unclass(f)

S3 — Method Dispatch

  • S3 methods follow the name convention generic.class()

  • We may use s3_dispatch() to investigate the process of method dispatch for an S3 object

    Copy to clipboard
    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

    Copy to clipboard
    ftype(print.factor) # check if method
    ## [1] "S3" "method"
15 / 44

S3 — Method Dispatch

Use s3_get_method() to see the source code of a method which is not exported.

Example: lm.print()

Copy to clipboard
mod <- lm(price ~ carat, data = ggplot2::diamonds)
typeof(mod)
## [1] "list"
Copy to clipboard
class(mod)
## [1] "lm"
16 / 44

S3 Basics — Method Dispatch

Example: lm.print() — ctd.

Copy to clipboard
# Inspect hidden print method for lm objects
s3_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>
17 / 44

S3 Basics — Exercises

  1. Describe the difference between t.test() and t.data.frame(). When is each function called?

  2. What class of object does the following code return? What base type is it built on? What attributes does it use?

    Copy to clipboard
    x <- ecdf(rpois(100, 10))
    x
  3. What class of object does the following code return? What base type is it built on? What attributes does it use?

    Copy to clipboard
    x <- table(rpois(100, 5))
    x
18 / 44

S3 Classes

  • 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!

    Copy to clipboard
    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)
19 / 44

S3 Classes — Robust Implementation

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

20 / 44

Case Study — An S3 Class for Prime Numbers

We now demonstrate how to implement a simple S3 class for prime numbers.

S3 — constructur function for prime class

Let's start with a constructor function which accepts integer input and returns an object with classes 'data frame' and 'prime'.

Copy to clipboard
new_prime <- function(x = integer()) {
stopifnot(is.integer(x))
x <- list(x)
class(x) <- 'prime'
x
}
21 / 44

Case Study — An S3 Class for Prime Numbers

S3 — validator function for prime class

The 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>0y[2,3,...,z1].

Copy to clipboard
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?)

22 / 44

Case Study — An S3 Class for Prime Numbers

S3 — helper function for prime class

At last we provide a user-friendly helper function which generates and validates prime objects.

Copy to clipboard
prime <- function(x = integer()) {
validate_prime(new_prime(x))
}
Copy to clipboard
(x <- prime(c(3L, 5L, 7L, 11L)))
## [[1]]
## [1] 3 5 7 11
##
## attr(,"class")
## [1] "prime"
## [1] "prime"
23 / 44

S3 Generics and Methods

  • Many prominent R functions are generics, e.g. mean(). Method dispatch is done by UseMethod().

    Copy to clipboard
    mean
    ## function (x, ...)
    ## UseMethod("mean")
    ## <bytecode: 0x7fbef74f1720>
    ## <environment: namespace:base>
  • Let's see the dispatch for mean(x) if x has class Date:

    Copy to clipboard
    x <- Sys.Date
    s3_dispatch(mean(x))
    ## mean.function
    ## => mean.default
24 / 44

S3 Generics and Methods

  • 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

    Copy to clipboard
    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
25 / 44

S3 Generics and Methods

s3_method_class() provides all registered methods for a given class.

Copy to clipboard
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
26 / 44

S3 Generics and Methods

Example: How to write a generic

Writing your own generic is straightforward

Copy to clipboard
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

Copy to clipboard
my_generic.default <- function(x) {
x
}
my_generic.my_class <- function(x) {
cat("Output for class 'my_class':\n", x)
}
27 / 44

comment on rules for creating methods (CH 13.4.3 in Advanced R)

S3 Generics and Methods

Example: How to write a generic — ctd.

We next check that method dispatch works as desired:

Copy to clipboard
y <- structure(x<-1, class = 'my_class')
s3_dispatch(my_generic(x))
## my_generic.double
## my_generic.numeric
## => my_generic.default
Copy to clipboard
s3_dispatch(my_generic(y))
## => my_generic.my_class
## * my_generic.default
28 / 44

S3 Generics and Methods

Example: How to write a generic — ctd.

We next check that method dispatch works as desired:

Copy to clipboard
y <- structure(x<-1, class = 'my_class')
my_generic(x)
## [1] 1
Copy to clipboard
my_generic(y)
## Output for class 'my_class':
## 1
29 / 44

Case Study — A Plot Method for prime Objects

30 / 44

Case Study — A Plot Method for prime Objects

  • The image from the previous slide is inspired by this mathexchange post.

  • Make sure to checkout 3Blue1Brown's video for a nice explanation of what's going on. 🙃👍🏼


31 / 44

Case Study — A Plot Method for prime Objects

Let'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/

32 / 44

Case Study — A Plot Method for prime Objects

Copy to clipboard
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 ...

33 / 44

Case Study — A Plot Method for prime Objects

Copy to clipboard
# Subset of all prime numbers in 2,...,1e4
x <- 2:1e4
x <- x[sapply(unique(x), function(z) all(z %% 2:(z-1) > 0))]
# Assign 'prime' class
x <- prime(x)

Let's check the method dispatch for plot(x)

Copy to clipboard
s3_dispatch(plot(x))
## => plot.prime
## * plot.default
34 / 44

Case Study — A Plot Method for prime Objects

Copy to clipboard
plot(x, coord = "polar", pch = 19, cex = 0.5, col = "red")

35 / 44

Exercises

  1. 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?

  2. What generics does the table class have methods for?

  3. Implement a summary() method for objects of class prime. Use the following strategy:

    1. A summary() method should do some computations and return an object of class summary_print but not print the results

    2. A separate print() method for summary_prime objects should output the results in an appealing format

36 / 44

S3 Inheritance

  • 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)

37 / 44

S3 Inheritance

  • Note that we may assign multiple classes to an S3 object

    Copy to clipboard
    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.

38 / 44

S3 Inheritance

Copy to clipboard
# New generic
g <- 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 hierachies
ab <- structure(1, class = c("A", "B")); ba <- structure(1, class = c("B", "A"))
g(ab)
## [1] "A"
Copy to clipboard
g(ba)
## [1] "B"
39 / 44

S3 Inheritance

It's possible to force delegation to the method of the subsequent class (if defined) using NextMethod(). This provides a simple inheritance mechanism:

Copy to clipboard
g.C <- function(x) NextMethod()
ca <- structure(1, class = c("C", "A"))
cb <- structure(1, class = c("C", "B"))
g(ca)
## [1] "A"
Copy to clipboard
g(cb)
## [1] "B"

Notice that inheritance depends on how we define the class vector:

  • 'For object ca, class C inherits its g() method from class A'
  • 'For object cb, class C inherits its g() method from class B'
40 / 44

S3 Inheritance

We may use these mechanics to implement 2 (child) classes which inherit (parts of) their print() method from the prime class.

Example: Inheritance of print() method

Copy to clipboard
print.prime <- function(x) {
x[[1]]
}
Copy to clipboard
print.mersenne <- function(x) {
cat('Mersenne primes:', NextMethod(x))
}
print.fermat <- function(x) {
cat('Fermat primes:', NextMethod(x))
}
41 / 44

S3 Inheritance

Example: Inheritance of print() method — ctd.

Copy to clipboard
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
Copy to clipboard
y
## Fermat primes: 3 5 17 257
42 / 44

S3 Inheritance

Example: Inheritance of print() method — ctd.

Copy to clipboard
s3_dispatch(print(x))
## => print.mersenne
## -> print.prime
## * print.default
Copy to clipboard
s3_dispatch(print(y))
## => print.fermat
## -> print.prime
## * print.default

-> indicates the method the call to the original method has been delegated to.

43 / 44

S3 Inheritance — Exercises

  1. Adjust the constructor function new_prime() such that it allows for subclasses. Then implement a constructor function new_fermat() for objects with subclass fermat.

  2. What happens if you subset x (generated as below) using [[?

    Copy to clipboard
    x <- prime(c(2L, 3L, 5L))
  3. Implement a [[ method which preserves the prime class.

44 / 44

Introduction

  • 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

2 / 44
Paused

Help

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