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

Advanced R by Hadley Wickham

Chapter 14: R6

Ezra Porter

Nov 5 2020

1 / 30

Big Picture

  • In functional OOP, like S3, methods belong to functions.

  • In encapsulated OOP, like R6, methods belong to objects.



  • R6 objects are always modified in place and never copied on modify

    • Powerful for abstracting complex objects with lots of self-contained components you might want to update

    • Can produce spooky results and spookier code if you're not careful


All we'll need is

library(R6)
2 / 30

Creating

Create R6 classes by calling R6::R6Class() and passing a list of methods and fields

Using exercise 14.2.6.1 as an example:

BankAccount <- R6Class("BankAccount", list(
balance = 0,
deposit = function(x) {
self$balance <- self$balance + x
invisible(self)
},
withdraw = function(x) {
self$balance <- self$balance - x
invisible(self)
})
)

self$ lets methods reference other fields or methods internal to the object

3 / 30

Using

Create a new instance of a class with the $new() method

checking <- BankAccount$new()

Access fields and methods with $

checking$balance
## [1] 0
checking$deposit(10)
checking$balance
## [1] 10
4 / 30

Using cnt'd

14.2.1 Method Chaining

Methods called for their side-effects (like setting internal values) can be chained together

checking$withdraw(10)$withdraw(10)
checking$balance
## [1] -10


This is powered by having side-effect methods return the object invisibly

withdraw = function(x) {
self$balance <- self$balance - x
invisible(self)
}
5 / 30

14.2.2 Important Methods

Some methods affect the behavior of objects in special ways

$initialize() overrides the default behavior of $new()

BankAccount <- R6Class("BankAccount", list(
balance = 0,
pwd = NULL,
initialize = function(pwd) {
self$pwd <- pwd
},
deposit = function(x, pwd) {
stopifnot(pwd == self$pwd)
self$balance <- self$balance + x
invisible(self)
},
withdraw = function(x, pwd) {
stopifnot(pwd == self$pwd)
self$balance <- self$balance - x
invisible(self)
})
)
6 / 30

14.2.2 Important Methods

Some methods affect the behavior of objects in special ways

$print() determines how the object is printed

BankAccount <- R6Class("BankAccount", list(
...
print = function(...) {
cat("Balance:", scales::dollar(self$balance))
invisible(self)
}
...
savings <- BankAccount$new(pwd = "dont-tell")
try(savings$deposit(10, "password123"))
## Error in savings$deposit(10, "password123") : pwd == self$pwd is not TRUE
(savings$deposit(10, "dont-tell"))
## Balance: $10
7 / 30

14.2.2 Important Methods

Beware! Objects encapsulate methods so our old BankAccount objects don't retroactively get newly created methods

checking
## <BankAccount>
## Public:
## balance: -10
## clone: function (deep = FALSE)
## deposit: function (x)
## withdraw: function (x)
checking <- BankAccount$new(pwd = "dont-tell")
checking
## Balance: $0

Make sure you rebuild objects when you alter a class during interactive use

8 / 30

14.2.4 Inheritance

R6 classes can be subclasses of other R6 classes. Define that relationship using the inherit argument to R6Class()

SocialistBankAccount <- R6Class("SocialistBankAccount",
inherit = BankAccount,
public = list(
check_balance = function() {
if (self$balance > 100000) {
cat("From each according to their ability!")
self$balance <- 100000
} else if (self$balance < 0) {
cat("To each according to their need!")
self$balance <- 100
}
},
deposit = function(x, pwd) {
super$deposit(x, pwd)
self$check_balance()
},
withdraw = function(x, pwd) {
...
9 / 30

14.2.4 Inheritance

Our subclass inherits the methods and fields we don't explicitly overwrite from its super class

common_fund <- SocialistBankAccount$new(pwd = "dont-tell")
common_fund
## Balance: $0
10 / 30

14.2.4 Inheritance

Our subclass inherits the methods and fields we don't explicitly overwrite from its super class

common_fund <- SocialistBankAccount$new(pwd = "dont-tell")
common_fund
## Balance: $0

super$ allows us to refer to superclass methods and thereby "delegate" like with NextMethod() in S3

deposit = function(x, pwd) {
super$deposit(x, pwd)
self$check_balance()
}
11 / 30

14.2.4 Inheritance

deposit = function(x, pwd) {
super$deposit(x, pwd)
self$check_balance()
}
common_fund$withdraw(10, "dont-tell")
## To each according to their need!
common_fund
## Balance: $100
12 / 30

14.2.4 Inheritance

deposit = function(x, pwd) {
super$deposit(x, pwd)
self$check_balance()
}
common_fund$withdraw(10, "dont-tell")
## To each according to their need!
common_fund
## Balance: $100

R6 objects also get S3 classes which automatically reproduce the sub/superclass relationships

class(common_fund)
## [1] "SocialistBankAccount" "BankAccount" "R6"
13 / 30

14.3 Controlling Access

Right now users have full access to internal elements of our objects

checking$pwd
## [1] "dont-tell"

We can use the private argument of R6Class() to set components for internal use

SecureBankAccount <- R6Class("SecureBankAccount",
public = list(
balance = 0,
initialize = function(pwd) {
private$pwd <- pwd
},
... More methods ...
),
private = list(pwd = NULL)
)
14 / 30

14.3 Controlling Access

SecureBankAccount <- R6Class("SecureBankAccount",
public = list(
balance = 0,
initialize = function(pwd) {
private$pwd <- pwd
},
... More methods ...
),
private = list(pwd = NULL)
)
secure_checking <- SecureBankAccount$new("dont-tell")
secure_checking$pwd
## NULL

Just reference private$ in methods rather than self$

15 / 30

14.4 Reference Semantics

R6 objects are always modified in place. To get a copy you can use the $clone() method.

16 / 30

14.4 Reference Semantics

R6 objects are always modified in place. To get a copy you can use the $clone() method.

The fact that methods of an object can change the object itself makes code harder to reason about.

Hadley's example:

What can we say about the effect of this line of code on x and y given that they're base objects? Given that they're R6 objects?

z <- f(x, y)
17 / 30

14.4 Reference Semantics

R6 objects are always modified in place. To get a copy you can use the $clone() method.

The fact that methods of an object can change the object itself makes code harder to reason about.

Hadley's example:

What can we say about the effect of this line of code on x and y given that they're base objects? Given that they're R6 objects?

z <- f(x, y)


If f calls methods of x and y it might change them. In our BankAccount example the only thing our methods did was change internal values.

18 / 30

14.4 Reference Semantics

But therein lies the power:

14.6.2.3 Why can’t you model a bank account or a deck of cards with an S3 class?
19 / 30

14.4 Reference Semantics

But therein lies the power:

14.6.2.3 Why can’t you model a bank account or a deck of cards with an S3 class?


S3 objects are copied when they're changed so the best you could do is have a generic function return a modified version of the object

20 / 30

14.4.3 R6 Fields (a cautionary tale)

R6 objects behave unintuitively when the default value of a field is another R6 object

Number <- R6Class("Number", list(
value = 0,
increment = function() {
self$value <- self$value + 1
})
)
NumberPointer <- R6Class("NumberPointer", list(
number = Number$new()
))


The instance of Number will be shared across all instances of NumberPointer

21 / 30

14.4.3 R6 Fields (a cautionary tale)

x <- NumberPointer$new()
y <- NumberPointer$new()
22 / 30

14.4.3 R6 Fields (a cautionary tale)

x <- NumberPointer$new()
y <- NumberPointer$new()
x$number$value
## [1] 0
23 / 30

14.4.3 R6 Fields (a cautionary tale)

x <- NumberPointer$new()
y <- NumberPointer$new()
x$number$value
## [1] 0
y$number$value
## [1] 0
24 / 30

14.4.3 R6 Fields (a cautionary tale)

x <- NumberPointer$new()
y <- NumberPointer$new()
x$number$value
## [1] 0
y$number$value
## [1] 0
x$number$increment()
25 / 30

14.4.3 R6 Fields (a cautionary tale)

x <- NumberPointer$new()
y <- NumberPointer$new()
x$number$value
## [1] 0
y$number$value
## [1] 0
x$number$increment()
x$number$value
## [1] 1
26 / 30

14.4.3 R6 Fields (a cautionary tale)

x <- NumberPointer$new()
y <- NumberPointer$new()
x$number$value
## [1] 0
y$number$value
## [1] 0
x$number$increment()
x$number$value
## [1] 1
y$number$value
## [1] 1
27 / 30

14.4.3 R6 Fields (a cautionary tale)

x <- NumberPointer$new()
y <- NumberPointer$new()
x$number$value
## [1] 0
y$number$value
## [1] 0
x$number$increment()
x$number$value
## [1] 1
y$number$value
## [1] 1

Avoid this by making sure objects are initialized within a method so you get a new instance every time

28 / 30

How have I used R6?

I haven't!

29 / 30

How have I used R6?

I haven't!

But this reminded me of how some machine learning and optimization algorithms are implemented in Python

30 / 30

Big Picture

  • In functional OOP, like S3, methods belong to functions.

  • In encapsulated OOP, like R6, methods belong to objects.



  • R6 objects are always modified in place and never copied on modify

    • Powerful for abstracting complex objects with lots of self-contained components you might want to update

    • Can produce spooky results and spookier code if you're not careful


All we'll need is

library(R6)
2 / 30
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