Chapter 14 R6
14.2 Classes and methods
Any … R6 method called for its side effects … should return invisible(self).
WHY? What is invisible(self)
doing in the add function below? I tried removing this line but it doesn’t change the output…
Accumulator <- R6Class("Accumulator", list(
sum = 0,
add = function(x = 1) {
self$sum <- self$sum + x
invisible(self)
})
)
x <- Accumulator$new()
Returning self
or invisible(self)
allows for method chaining. i.e. x$add(1)$add(2)
can be done rather than x$add(1); x$add(2)
. In the absence of anything better to return, why not return itself
14.2.2 Important methods
Why don’t we need to specifically call haldey2$print
to see the output below)? Is the print method just a built in output of an R6
object?
Person <- R6Class("Person", list(
name = NULL,
age = NA,
initialize = function(name, age = NA) {
self$name <- name
self$age <- age
},
print = function(...) {
cat("Person: \n")
cat(" Name: ", self$name, "\n", sep = "")
cat(" Age: ", self$age, "\n", sep = "")
invisible(self)
}
))
hadley2 <- Person$new("Hadley")
hadley2
Person:
Name: Hadley
Age: NA
Print is an S3 generic, so when print is implicitly called, it calls the method for the R6 object. The method for the R6 object is “look for a print function inside the R6 object”
Classes and methods 14.2.3
Once instantiated, can you add another method to an R6
object or does the set
function only work on the R6
generator?
Once instantiated the environments are locked (no new bindings). But, you can fudge with things and still modify existing bindings if you wanted. But, this is not really something to advocate:
Beer <- R6::R6Class(
'Beer',
public = list(
abv = 0.05,
percent_abv = function() { sprintf('%.1f%%', 100 * self$abv) }
)
)
class(Beer)
#> [1] "R6ClassGenerator"
beer <- Beer$new()
unlockBinding("percent_abv", beer)
beer$percent_abv <- function() { cat("h@xed")}
lockBinding("percent_abv", beer)
beer$percent_abv()
[1] "R6ClassGenerator"
h@xed
You can also set the lock_objects option on the generator to FALSE
Beer <- R6::R6Class(
'Beer',
public = list(
abv = 0.05,
percent_abv = function() { sprintf('%.1f%%', 100 * self$abv) }
),
lock_objects = FALSE
)
beer <- Beer$new()
beer$rating <- 5
beer$rating
## [1] 5
[1] 5
But this still won’t work because the bindings were still locked
#> Error in beer$percent_abv <- function() {: cannot change value of locked binding for 'percent_abv'
Exercises 14.2.6.4
Why can’t we use method chaining to access the current time zone?
Timezone <- R6Class(
classname = "Timezone",
public = list(
get = function() {
Sys.timezone()
},
set = function(value) {
stopifnot(value %in% OlsonNames())
old <- self$get()
Sys.setenv(TZ = value)
invisible(old)
})
)
tz <- Timezone$new()
old <- tz$set("Antarctica/South_Pole")
tz$get()
[1] "America/Los_Angeles"
Error in tz$set("Antarctica/South_Pole")$get :
$ operator is invalid for atomic vectors
You’re not returning an R6 object in your first “set”, you’re returning a character vector, so you can’t find the get method of that character vector
We can return something using the following code:
Timezone <- R6Class(
classname = "Timezone",
public = list(
get = function() {
self$current_zone
},
set = function(v) {
self$current_zone <- v
invisible(self)
},
reset = function() {
if (!is.null(private$.old)) {
self$current_zone <- private$.old
}
invisible(self)
}
),
active = list(
current_zone = function(v) {
if (missing(v)) {
return(Sys.timezone())
}
stopifnot(v %in% OlsonNames())
old <- Sys.timezone()
if (Sys.setenv(TZ=v)) {
private$.old <- old
} else {
stop("Unable to set timezone.")
}
invisible(self)
}
),
private = list(
.old = NULL
)
)
tz <- Timezone$new()
tz$set("Antarctica/South_Pole")$get()
#> [1] "Antarctica/South_Pole"
tz$reset()$get()
#> [1] "US/Central"
That said, not every function should support chaining. Sometimes you want the function to return some value (like $get
) and in these cases, you just won’t be able to chain, and that is okay.
14.4 Reference semantics
$clone() does not recursively clone nested R6 objects. If you want that, you’ll need to use $clone(deep = TRUE).
Can we see this in action using a subclass?
This example is taken from the R6
documentation:
Object c1
contains s
, which we will clone. The original and clone both point to the same object, and by using deep = TRUE
we can modify s
in one object without changing it in the other.
Simple <- R6Class("Simple", public = list(x = 1))
Cloneable <- R6Class("Cloneable",
public = list(
s = NULL,
initialize = function() self$s <- Simple$new()
)
)
c1 <- Cloneable$new()
c2 <- c1$clone()
# Change c1's `s` field
c1$s$x <- 2
# c2's `s` is the same object, so it reflects the change
c2$s$x
#> [1] 2
c3 <- c1$clone(deep = TRUE)
# Change c1's `s` field
c1$s$x <- 3
# c2's `s` is different
# if we set deep = FALSE this would be 3!
c3$s$x
#> [1] 2
When or why would I set the clone argument to FALSE?
Clone has a large memory footprint so if you’re going to create a lot of R6 methods you may want to exclude this!