14.2 Classes and methods

R6 only needs a single function call to create both the class and its methods: R6::R6Class(). This is the only function from the package that you’ll ever use!51

The following example shows the two most important arguments to R6Class():

  • The first argument is the classname. It’s not strictly needed, but it improves error messages and makes it possible to use R6 objects with S3 generics. By convention, R6 classes have UpperCamelCase names.

  • The second argument, public, supplies a list of methods (functions) and fields (anything else) that make up the public interface of the object. By convention, methods and fields use snake_case. Methods can access the methods and fields of the current object via self$.52

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

You should always assign the result of R6Class() into a variable with the same name as the class, because R6Class() returns an R6 object that defines the class:

Accumulator
#> <Accumulator> object generator
#>   Public:
#>     sum: 0
#>     add: function (x = 1) 
#>     clone: function (deep = FALSE) 
#>   Parent env: <environment: R_GlobalEnv>
#>   Locked objects: TRUE
#>   Locked class: FALSE
#>   Portable: TRUE

You construct a new object from the class by calling the new() method. In R6, methods belong to objects, so you use $ to access new():

x <- Accumulator$new() 

You can then call methods and access fields with $:

x$add(4) 
x$sum
#> [1] 4

In this class, the fields and methods are public, which means that you can get or set the value of any field. Later, we’ll see how to use private fields and methods to prevent casual access to the internals of your class.

To make it clear when we’re talking about fields and methods as opposed to variables and functions, I’ll prefix their names with $. For example, the Accumulate class has field $sum and method $add().

14.2.1 Method chaining

$add() is called primarily for its side-effect of updating $sum.

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

Side-effect R6 methods should always return self invisibly. This returns the “current” object and makes it possible to chain together multiple method calls:

x$add(10)$add(10)$sum
#> [1] 24

For, readability, you might put one method call on each line:

x$
  add(10)$
  add(10)$
  sum
#> [1] 44

This technique is called method chaining and is commonly used in languages like Python and JavaScript. Method chaining is deeply related to the pipe, and we’ll discuss the pros and cons of each approach in Section 16.3.3.

14.2.2 Important methods

There are two important methods that should be defined for most classes: $initialize() and $print(). They’re not required, but providing them will make your class easier to use.

$initialize() overrides the default behaviour of $new(). For example, the following code defines an Person class with fields $name and $age. To ensure that that $name is always a single string, and $age is always a single number, I placed checks in $initialize().

Person <- R6Class("Person", list(
  name = NULL,
  age = NA,
  initialize = function(name, age = NA) {
    stopifnot(is.character(name), length(name) == 1)
    stopifnot(is.numeric(age), length(age) == 1)
    
    self$name <- name
    self$age <- age
  }
))

hadley <- Person$new("Hadley", age = "thirty-eight")
#> Error in .subset2(public_bind_env, "initialize")(...): is.numeric(age) is not
#> TRUE

hadley <- Person$new("Hadley", age = 38)

If you have more expensive validation requirements, implement them in a separate $validate() and only call when needed.

Defining $print() allows you to override the default printing behaviour. As with any R6 method called for its side effects, $print() should return invisible(self).

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

This code illustrates an important aspect of R6. Because methods are bound to individual objects, the previously created hadley object does not get this new method:

hadley
#> <Person>
#>   Public:
#>     age: 38
#>     clone: function (deep = FALSE) 
#>     initialize: function (name, age = NA) 
#>     name: Hadley

hadley$print
#> NULL

From the perspective of R6, there is no relationship between hadley and hadley2; they just coincidentally share the same class name. This doesn’t cause problems when using already developed R6 objects but can make interactive experimentation confusing. If you’re changing the code and can’t figure out why the results of method calls aren’t any different, make sure you’ve re-constructed R6 objects with the new class.

14.2.3 Adding methods after creation

Instead of continuously creating new classes, it’s also possible to modify the fields and methods of an existing class. This is useful when exploring interactively, or when you have a class with many functions that you’d like to break up into pieces. Add new elements to an existing class with $set(), supplying the visibility (more on in Section 14.3), the name, and the component.

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

As above, new methods and fields are only available to new objects; they are not retrospectively added to existing objects.

14.2.4 Inheritance

To inherit behaviour from an existing class, provide the class object to the inherit argument:

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

$add() overrides the superclass implementation, but we can still delegate to the superclass implementation by using super$. (This is analogous to NextMethod() in S3, as discussed in Section 13.6.) Any methods which are not overridden will use the implementation in the parent class.

14.2.5 Introspection

Every R6 object has an S3 class that reflects its hierarchy of R6 classes. This means that the easiest way to determine the class (and all classes it inherits from) is to use class():

class(hadley2)
#> [1] "Person" "R6"

The S3 hierarchy includes the base “R6” class. This provides common behaviour, including a print.R6() method which calls $print(), as described above.

You can list all methods and fields with names():

names(hadley2)
#> [1] ".__enclos_env__" "age"             "name"            "clone"          
#> [5] "print"           "initialize"

We defined $name, $age, $print, and $initialize. As suggested by the name, .__enclos_env__ is an internal implementation detail that you shouldn’t touch; we’ll come back to $clone() in Section 14.4.

14.2.6 Exercises

  1. Create a bank account R6 class that stores a balance and allows you to deposit and withdraw money. Create a subclass that throws an error if you attempt to go into overdraft. Create another subclass that allows you to go into overdraft, but charges you a fee.

  2. Create an R6 class that represents a shuffled deck of cards. You should be able to draw cards from the deck with $draw(n), and return all cards to the deck and reshuffle with $reshuffle(). Use the following code to make a vector of cards.

    suit <- c("♠", "♥", "♦", "♣")
    value <- c("A", 2:10, "J", "Q", "K")
    cards <- paste0(rep(value, 4), suit)
  3. Why can’t you model a bank account or a deck of cards with an S3 class?

  4. Create an R6 class that allows you to get and set the current timezone. You can access the current timezone with Sys.timezone() and set it with Sys.setenv(TZ = "newtimezone"). When setting the time zone, make sure the new time zone is in the list provided by OlsonNames().

  5. Create an R6 class that manages the current working directory. It should have $get() and $set() methods.

  6. Why can’t you model the time zone or current working directory with an S3 class?

  7. What base type are R6 objects built on top of? What attributes do they have?


  1. That means if you’re creating R6 in a package, you only need to make sure it’s listed in the Imports field of the DESCRIPTION. There’s no need to import the package into the NAMESPACE.↩︎

  2. Unlike in this in python, the self variable is automatically provided by R6, and does not form part of the method signature.↩︎