Skip to content
AgileLogics
6 min read

S4 and R6 Classes

Prerequisites: Object-oriented R: S3, Environments, scoping, and closures

S3 is informal: nothing stops you from creating a malformed object. Two other systems add structure.

S4 — formal classes

S4 declares classes with typed fields (slots) and validity checks. It's used heavily in Bioconductor (bioinformatics) packages.

setClass("Account", representation(owner = "character", balance = "numeric"),
         validity = function(object) {
           if (object@balance < 0) "balance can't be negative" else TRUE
         })

setGeneric("deposit", function(account, amount) standardGeneric("deposit"))
setMethod("deposit", "Account", function(account, amount) {
  account@balance <- account@balance + amount
  validObject(account)
  account
})

acc <- new("Account", owner = "Ada", balance = 100)
acc <- deposit(acc, 50)
acc@balance
tryCatch(new("Account", owner = "Bob", balance = -5), error = \(e) cat("Error caught\n"))
[1] "deposit"
[1] 150
Error caught

(setGeneric prints the name of the generic it created — that's the first line.)

  • Slots are accessed with @.
  • Methods are defined with setGeneric and setMethod.
  • Like most R objects, S4 objects have copy semantics: deposit returns a modified copy, which is why the result is reassigned to acc.

R6 — objects that change in place

The R6 package provides classes that work like those in Python or Java: methods belong to the object, and the object is modified in place (reference semantics).

library(R6)

Account <- R6Class("Account",
  public = list(
    owner = NULL,
    initialize = function(owner, balance = 0) {
      self$owner <- owner
      private$balance <- balance
    },
    deposit = function(amount) {
      stopifnot(amount > 0)
      private$balance <- private$balance + amount
      invisible(self)
    },
    get_balance = function() private$balance
  ),
  private = list(balance = 0)
)

acc <- Account$new("Ada", 100)
acc$deposit(50)$deposit(25)
acc$get_balance()

alias <- acc
alias$deposit(10)
acc$get_balance()
[1] 175
[1] 185
  • self refers to the object; private fields are hidden from outside.
  • Returning invisible(self) lets methods be chained.
  • alias <- acc doesn't copy: both names refer to the same object, so depositing through alias changes acc. (Use acc$clone() for a copy.)

Which to choose

System Use when
S3 Adding print/summary/format methods to your data — most cases
S4 Working in Bioconductor, or you need formal classes and multiple dispatch
R6 Objects with changing state — connections, caches, simulations, Shiny modules

Most R code is functional — data in, data out. Reach for objects when they genuinely model something with identity and state.