R6

Learning objectives:

  • Create R6 classes
  • Recognize main R6 properties
  • Assign R6 methods and fields
  • Compare different R6 fields
  • Show R6 inheritance pattern
  • Implement R6 finalizers
library(R6)

R6 classes

R6Class creates the R6 reference object

  • Empty is an environment
Empty <- R6Class("Empty")
Empty
#> <Empty> object generator
#>   Public:
#>     clone: function (deep = FALSE) 
#>   Parent env: <environment: R_GlobalEnv>
#>   Locked objects: TRUE
#>   Locked class: FALSE
#>   Portable: TRUE

Every R6 object has an S3 class that reflects its hierarchy of R6 classes.

e <- Empty$new()
class(e)
#> [1] "Empty" "R6"

R6 properties

R6 objects have methods, not generics

  • add is a method of the Accumulator object.
  • Can’t use sum and add outside this class
Accumulator <- R6Class("Accumulator", list(
  sum = 0,
  add = function(x = 1) {
    self$sum <- self$sum + x
    invisible(self)
  }
))

R6 objects have reference semantics

  • Use the $clone() method to copy the object
x <- Accumulator$new()
x$add(4)
x$sum
#> [1] 4
y <- Accumulator$new()
y$sum
#> [1] 0
z <- x$clone()
z$sum
#> [1] 4

R6 methods can be chained resembling |> syntax

  • All side-effect R6 methods should return self invisibly.
  • This allows for method chaining.
x$add(10)$add(10)$sum
#> [1] 24
x$
  add(10)$
  add(10)$
  sum
#> [1] 44

R6 methods and fields

$initialize() overides the default behaviour of $new()

Person <- R6Class("Person",list(
  name = NULL,
  initialize = function(name) {
    stopifnot(is.character(name))
    stopifnot(length(name)==1)
    self$name <- name
    invisible(self)
  },
  print = function(...) {
    cat("Person\n")
    cat("   name: ",self$name,"\n")
    invisible(self)
  }
))
Person$new("Nick")
#> Person
#>    name:  Nick
Person$new(42)
#> Error in initialize(...): is.character(name) is not TRUE

$set() assigns methods after creating R6 objects

Note

Keep in mind methods added with $set() are only available with new objects.

Accumulator <- R6Class("Accumulator")
Accumulator$set("public", "sum", 0)
Accumulator$set("public", "add", function(x = 1) {
  self$sum <- self$sum + x
  invisible(self)
})

Private vs. public vs. active R6 fields

Accessing private R6 fields yield NULL

Transaction <- R6Class("Transaction", 
  public = list(
    last_transaction = 0,
    initialize = function(owner) {
      private$owner <- owner
      invisible(self)
    },
    deposit = function(amount) {
      private$balance <- private$balance + amount
      self$last_transaction <- amount
      invisible(self)
    },
    withdraw = function(amount) {
      private$balance <- private$balance - amount
      self$last_transaction <- -amount
      invisible(self)
    },
    print = function(...) {
      cat("Transactions by ",private$owner,"\n")
      cat("  Last transaction: ",self$last_transaction,"\n")
      cat("  Balance: ",private$balance,"\n")
    }
  ),
  private = list(
    owner = NULL,
    balance = 0
  )
)
t <- Transaction$new("Nick")
t$owner
#> NULL

Public methods like $print() can reveal private fields

t$balance
#> NULL
t$deposit(20)$withdraw(13)$withdraw(2)
t$last_transaction
#> [1] -2
t
#> Transactions by  Nick 
#>   Last transaction:  -2 
#>   Balance:  5

Active fields hide complex method calls

Rando <- R6::R6Class("Rando", active = list(
  random = function(value) {
    if (missing(value)) {
      runif(1)  
    } else {
      stop("Can't set `$random`", call. = FALSE)
    }
  }
))
x <- Rando$new()
x$random
#> [1] 0.4715425
x$random
#> [1] 0.1138315
x$random
#> [1] 0.1306242

Active fields hide complexity like validating user input

Person <- R6Class("Person", 
  private = list(
    .age = NA,
    .name = NULL
  ),
  active = list(
    age = function(value) {
      if (missing(value)) {
        private$.age
      } else {
        stop("`$age` is read only", call. = FALSE)
      }
    },
    name = function(value) {
      if (missing(value)) {
        private$.name
      } else {
        stopifnot(is.character(value), length(value) == 1)
        private$.name <- value
        self
      }
    }
  ),
  public = list(
    initialize = function(name, age = NA) {
      private$.name <- name
      private$.age <- age
    }
  )
)

nick <- Person$new("Nick", age = 33)
nick$name
#> [1] "Nick"
nick$name <- 10
#> Error in (function (value) : is.character(value) is not TRUE
nick$age <- 20
#> Error: `$age` is read only

R6 object inheritance

inherit allows providing behavior from existing R6 classes

  • The add method was modified for a more verbose behavior
  • super$add() is the add method call from the inherited object
AccumulatorChatty <- R6Class("AccumulatorChatty", 
  inherit = Accumulator,
  public = list(
    add = function(x = 1) {
      cat("Adding ", x, "\n", sep = "")
      super$add(x = x)
    }
  )
)

x2 <- AccumulatorChatty$new()
x2$add(10)$add(1)$sum
#> Adding 10
#> Adding 1
#> [1] 11

R6 finalizers for clean up duty