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
setGenericandsetMethod. - Like most R objects, S4 objects have copy semantics:
depositreturns a modified copy, which is why the result is reassigned toacc.
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
selfrefers to the object;privatefields are hidden from outside.- Returning
invisible(self)lets methods be chained. alias <- accdoesn't copy: both names refer to the same object, so depositing throughaliaschangesacc. (Useacc$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.