S4 Methods & Multiple Dispatch in R: setMethod() Explained
S4 methods use formal generic functions and support multiple dispatch -- choosing a method based on the classes of multiple arguments, not just the first one. This is S4's most powerful feature and what sets it apart from S3.
In S3, dispatch looks only at the class of the first argument. S4 can look at the classes of two, three, or more arguments simultaneously. This lets you write methods like "when a Matrix meets a SparseMatrix, do this; when two dense Matrices meet, do that."
Creating Generics with setGeneric()
Before defining methods, you need a generic function. setGeneric() declares the interface.
# Define an S4 generic
setGeneric("area", function(shape) {
standardGeneric("area")
})
# Define the class
setClass("Circle", slots = list(radius = "numeric"))
setClass("Rectangle", slots = list(width = "numeric", height = "numeric"))
# Define methods for each class
setMethod("area", "Circle", function(shape) {
pi * shape@radius^2
})
setMethod("area", "Rectangle", function(shape) {
shape@width * shape@height
})
# Test
c1 <- new("Circle", radius = 5)
r1 <- new("Rectangle", width = 4, height = 6)
cat("Circle area:", area(c1), "\n")
cat("Rectangle area:", area(r1), "\n")
Method Signatures
The signature argument in setMethod() specifies which argument classes the method handles. For single dispatch, you pass just the class name. For multiple dispatch, you specify classes for multiple arguments.
# A generic with two arguments
setGeneric("combine", function(a, b) {
standardGeneric("combine")
})
setClass("TextData", slots = list(text = "character"))
setClass("NumData", slots = list(values = "numeric"))
# Method for TextData + TextData
setMethod("combine", signature("TextData", "TextData"), function(a, b) {
new("TextData", text = paste(a@text, b@text))
})
# Method for NumData + NumData
setMethod("combine", signature("NumData", "NumData"), function(a, b) {
new("NumData", values = c(a@values, b@values))
})
# Method for TextData + NumData (mixed!)
setMethod("combine", signature("TextData", "NumData"), function(a, b) {
new("TextData", text = paste(a@text, paste(b@values, collapse = ",")))
})
# Test different combinations
t1 <- new("TextData", text = "hello")
t2 <- new("TextData", text = "world")
n1 <- new("NumData", values = c(1, 2, 3))
n2 <- new("NumData", values = c(4, 5))
cat("Text + Text:", combine(t1, t2)@text, "\n")
cat("Num + Num:", combine(n1, n2)@values, "\n")
cat("Text + Num:", combine(t1, n1)@text, "\n")
Multiple Dispatch in Action
Multiple dispatch shines when behavior depends on the combination of argument types. Here is a practical example with a formatting system.
# Define format classes
setClass("HTML", slots = list(content = "character"))
setClass("Markdown", slots = list(content = "character"))
setClass("PlainText", slots = list(content = "character"))
setGeneric("convert", function(from, to) {
standardGeneric("convert")
})
# Different conversions based on BOTH from and to types
setMethod("convert", signature("Markdown", "HTML"), function(from, to) {
# Simple markdown-to-HTML simulation
html <- gsub("\\*\\*(.+?)\\*\\*", "<b>\\1</b>", from@content)
html <- gsub("\\*(.+?)\\*", "<i>\\1</i>", html)
new("HTML", content = html)
})
setMethod("convert", signature("HTML", "PlainText"), function(from, to) {
plain <- gsub("<[^>]+>", "", from@content)
new("PlainText", content = plain)
})
setMethod("convert", signature("Markdown", "PlainText"), function(from, to) {
plain <- gsub("\\*+", "", from@content)
new("PlainText", content = plain)
})
# Test
md <- new("Markdown", content = "This is **bold** and *italic*")
html_out <- convert(md, new("HTML"))
plain_out <- convert(md, new("PlainText"))
cat("Markdown:", md@content, "\n")
cat("HTML:", html_out@content, "\n")
cat("Plain:", plain_out@content, "\n")
callNextMethod(): Inheritance in S4 Methods
callNextMethod() is S4's equivalent of S3's NextMethod(). It calls the method for the parent class.
You can define S4 methods for existing generics, including show (S4's equivalent of print), arithmetic operators, and comparison operators.
setClass("Money",
slots = list(amount = "numeric", currency = "character"),
prototype = list(currency = "USD")
)
# Override show (the S4 print method)
setMethod("show", "Money", function(object) {
symbol <- switch(object@currency, USD = "$", EUR = "\u20AC", GBP = "\u00A3", "")
cat(symbol, formatC(object@amount, format = "f", digits = 2), object@currency, "\n")
})
# Override + for Money
setMethod("+", signature("Money", "Money"), function(e1, e2) {
if (e1@currency != e2@currency) stop("Cannot add different currencies")
new("Money", amount = e1@amount + e2@amount, currency = e1@currency)
})
# Override comparison
setMethod("==", signature("Money", "Money"), function(e1, e2) {
e1@currency == e2@currency && e1@amount == e2@amount
})
# Test
a <- new("Money", amount = 100, currency = "USD")
b <- new("Money", amount = 50, currency = "USD")
show(a)
show(b)
result <- a + b
show(result)
cat("a == b:", a == b, "\n")
Inspecting Methods
# List all methods for a generic
cat("Methods for 'area':\n")
print(showMethods("area", printTo = FALSE))
# Check if a method exists for specific classes
cat("\nHas method for Circle?", hasMethod("area", "Circle"), "\n")
# Find which method would be selected
cat("\nMethod for Circle:\n")
print(getMethod("area", "Circle"))
# List all generics that have methods for a class
# showMethods(classes = "Circle") # lists all methods for Circle
Exercise 1: Create classes Vector2D and Vector3D. Define an add generic that works for all four combinations: 2D+2D, 3D+3D, 2D+3D (extend 2D to 3D with z=0), 3D+2D.
Click to reveal solution
```r
setClass("Vector2D", slots = list(x = "numeric", y = "numeric"))
setClass("Vector3D", slots = list(x = "numeric", y = "numeric", z = "numeric"))
setGeneric("add", function(a, b) standardGeneric("add"))
setMethod("add", signature("Vector2D", "Vector2D"), function(a, b) {
new("Vector2D", x = a@x + b@x, y = a@y + b@y)
})
setMethod("add", signature("Vector3D", "Vector3D"), function(a, b) {
new("Vector3D", x = a@x + b@x, y = a@y + b@y, z = a@z + b@z)
})
setMethod("add", signature("Vector2D", "Vector3D"), function(a, b) {
new("Vector3D", x = a@x + b@x, y = a@y + b@y, z = b@z)
})
setMethod("add", signature("Vector3D", "Vector2D"), function(a, b) {
new("Vector3D", x = a@x + b@x, y = a@y + b@y, z = a@z)
})
v2 <- new("Vector2D", x = 1, y = 2)
v3 <- new("Vector3D", x = 10, y = 20, z = 30)
r <- add(v2, v3)
cat("Result:", r@x, r@y, r@z, "\n")
Exercise 2: Define a show method for Vector2D that prints <1, 2> format, and a callNextMethod() chain with a parent class MathObject that prints "[MathObject]" after the vector representation.
Q: What is the difference between single and multiple dispatch? Single dispatch (S3) picks a method based on one argument's class. Multiple dispatch (S4) picks a method based on the classes of two or more arguments simultaneously. This is essential when the correct behavior depends on the combination of types.
Q: Can I mix S3 and S4 methods? Yes, but carefully. You can call setGeneric() on an existing S3 generic, and S4 methods will be found alongside S3 methods. Use setOldClass() to register S3 classes for use in S4 signatures.
Q: When would I use callNextMethod() vs writing separate methods? Use callNextMethod() when child classes should extend (not replace) parent behavior. If the child method does something completely different, write a standalone method without calling the parent.
What's Next
R6 Classes in R -- Reference semantics and mutable state with the R6 package
S3 Method Dispatch -- How UseMethod() and NextMethod() work under the hood