R OOP with S4

S4 is R's formal object-oriented system. Unlike S3, S4 requires explicit class definitions with typed slots, formal method signatures, and validation rules. It is stricter, more structured, and better suited for large, collaborative projects where data integrity is critical.

S3 vs S4 Comparison

Feature              S3                     S4
──────────────────────────────────────────────────────────────────
Class definition     Implicit (assign class) Explicit setClass()
Slot access          $ operator             @ operator
Slot types           No enforcement         Enforced (typed slots)
Method definition    function.class()       setMethod()
Validation           Manual                 validity= argument
Inheritance          c("Child","Parent")    contains= argument
Checking             class(x)               is(x, "ClassName")

Defining an S4 Class

setClass("BankAccount", representation(
  owner   = "character",
  balance = "numeric",
  id      = "character"
), validity = function(object) {
  if (object@balance < 0) "Balance cannot be negative"
  else TRUE
})

Creating S4 Objects with new()

acc1 <- new("BankAccount",
             owner   = "Asha Sharma",
             balance = 10000,
             id      = "ACC-001")

# Access slots with @
acc1@owner     # "Asha Sharma"
acc1@balance   # 10000
acc1@id        # "ACC-001"

# Check object
isVirtualClass("BankAccount")   # FALSE
is(acc1, "BankAccount")         # TRUE
slotNames(acc1)                 # "owner" "balance" "id"

Defining S4 Methods

# Generic function (define once)
setGeneric("deposit", function(account, amount) {
  standardGeneric("deposit")
})

# Method for BankAccount
setMethod("deposit", "BankAccount", function(account, amount) {
  if (amount <= 0) stop("Deposit amount must be positive")
  account@balance <- account@balance + amount
  cat("Deposited ₹", amount, "| New balance: ₹", account@balance, "\n")
  account
})

acc1 <- deposit(acc1, 5000)
# Deposited ₹ 5000 | New balance: ₹ 15000

show() Method (like print for S4)

setMethod("show", "BankAccount", function(object) {
  cat("Account ID:  ", object@id,      "\n")
  cat("Owner:       ", object@owner,   "\n")
  cat("Balance:    ₹", object@balance, "\n")
})

acc1     # automatically calls show()
# Account ID:   ACC-001
# Owner:        Asha Sharma
# Balance:    ₹ 15000

Inheritance with S4

setClass("SavingsAccount",
  contains   = "BankAccount",   # inherits BankAccount
  representation(
    interest_rate = "numeric"
  )
)

setMethod("show", "SavingsAccount", function(object) {
  callNextMethod()    # calls BankAccount show()
  cat("Interest Rate:", object@interest_rate * 100, "%\n")
})

savings <- new("SavingsAccount",
                owner="Balu", balance=20000,
                id="SAV-001", interest_rate=0.06)
savings
# Account ID:   SAV-001
# Owner:        Balu
# Balance:    ₹ 20000
# Interest Rate: 6 %

Multiple Dispatch

# S4 allows methods that dispatch on the type of MULTIPLE arguments
setGeneric("transfer", function(from, to, amount) standardGeneric("transfer"))

setMethod("transfer",
  signature("BankAccount","BankAccount","numeric"),
  function(from, to, amount) {
    from@balance <- from@balance - amount
    to@balance   <- to@balance   + amount
    cat("Transferred ₹", amount, "from", from@id, "to", to@id, "\n")
    list(from=from, to=to)
  }
)

Validity Checking

# validObject() explicitly checks the validity function
tryCatch({
  bad_acc <- new("BankAccount", owner="X", balance=-500, id="BAD")
  validObject(bad_acc)
}, error = function(e) cat("Validation Error:", conditionMessage(e), "\n"))
# Validation Error: Balance cannot be negative

S4 shines in large, multi-developer projects where strict type enforcement prevents data corruption. The Bioconductor project — which powers most computational biology in R — uses S4 extensively. For smaller projects, S3 or R6 is usually simpler. Choose S4 when you need guaranteed slot types, multiple dispatch, and formal inheritance hierarchies.

Leave a Comment

Your email address will not be published. Required fields are marked *