S3 Method Dispatch in R: UseMethod(), NextMethod() & Inheritance
When you call print(x), R looks at the class of x and finds the right print method to run. This process is called S3 method dispatch. Understanding it lets you write classes that behave correctly with inheritance, custom generics, and group generics.
S3 is the most widely used OOP system in R. Every time you call summary(), plot(), or print(), the dispatch mechanism silently routes your call to the right method. This tutorial dives into the machinery behind it.
How UseMethod() Works
Every S3 generic function contains a call to UseMethod(). When invoked, R inspects the class of the first argument and searches for a matching method.
# A simple generic function
speak <- function(animal, ...) {
UseMethod("speak")
}
# Methods for specific classes
speak.Dog <- function(animal, ...) cat(animal$name, "says: Woof!\n")
speak.Cat <- function(animal, ...) cat(animal$name, "says: Meow!\n")
# The default method (fallback)
speak.default <- function(animal, ...) cat("Unknown animal sound\n")
# Test dispatch
dog <- structure(list(name = "Rex"), class = "Dog")
cat <- structure(list(name = "Whiskers"), class = "Cat")
fish <- structure(list(name = "Nemo"), class = "Fish")
speak(dog) # Dispatches to speak.Dog
speak(cat) # Dispatches to speak.Cat
speak(fish) # No speak.Fish -> falls to speak.default
The dispatch algorithm is straightforward:
Look at the class of the first argument
Search for generic.class (e.g., speak.Dog)
If not found and the object has multiple classes, try the next class
If nothing matches, try generic.default
If no default exists, throw an error
Method Resolution Order with Multiple Classes
An S3 object can have multiple classes (a class vector). R tries each class in order, left to right, until it finds a matching method.
# Create an object with multiple classes
golden <- structure(
list(name = "Buddy", breed = "Golden Retriever", is_service = TRUE),
class = c("ServiceDog", "Dog", "Pet")
)
# Define methods at different levels
describe <- function(x, ...) UseMethod("describe")
describe.Pet <- function(x, ...) cat(x$name, "is a pet\n")
describe.Dog <- function(x, ...) cat(x$name, "is a dog (breed:", x$breed, ")\n")
describe.ServiceDog <- function(x, ...) cat(x$name, "is a service dog\n")
# Dispatches to describe.ServiceDog (first class in vector)
describe(golden)
# Check the class vector
cat("Class vector:", class(golden), "\n")
# If we remove ServiceDog method, it falls through to Dog
rm(describe.ServiceDog)
describe(golden)
NextMethod(): Calling the Parent Method
NextMethod() tells R to continue dispatching to the next class in the inheritance chain. This is how you implement inheritance behavior (calling the parent's method from the child).
# A base "Shape" class
area <- function(shape, ...) UseMethod("area")
describe_shape <- function(shape, ...) UseMethod("describe_shape")
describe_shape.Shape <- function(shape, ...) {
cat("Shape with color:", shape$color, "\n")
}
describe_shape.Circle <- function(shape, ...) {
cat("Circle with radius:", shape$radius, "\n")
NextMethod() # Call describe_shape.Shape
}
describe_shape.ColoredCircle <- function(shape, ...) {
cat("A fancy colored circle!\n")
NextMethod() # Call describe_shape.Circle
}
# Create a multi-class object
cc <- structure(
list(radius = 5, color = "blue"),
class = c("ColoredCircle", "Circle", "Shape")
)
# Watch the chain: ColoredCircle -> Circle -> Shape
describe_shape(cc)
NextMethod() passes along the same arguments automatically. You rarely need to pass arguments explicitly.
R has four group generics that let you define behavior for a whole group of operations at once. Instead of defining +.MyClass, -.MyClass, *.MyClass individually, you define Ops.MyClass to cover them all.
Group
Functions covered
Ops
+, -, *, /, ^, %%, %/%, &, `
, !, ==, !=, <, <=, >=, >`
Math
abs, sqrt, ceiling, floor, log, exp, sin, cos, etc.
Complex
Re, Im, Mod, Arg, Conj
Summary
all, any, sum, prod, min, max, range
# Create a "Celsius" class
celsius <- function(temp) {
structure(list(value = temp), class = "Celsius")
}
print.Celsius <- function(x, ...) cat(x$value, "C\n")
# Define Ops for all arithmetic operators at once
Ops.Celsius <- function(e1, e2) {
# .Generic contains the operator being used (+, -, etc.)
v1 <- if (inherits(e1, "Celsius")) e1$value else e1
v2 <- if (inherits(e2, "Celsius")) e2$value else e2
result <- get(.Generic)(v1, v2)
# Comparison operators return logical, not Celsius
if (.Generic %in% c("==", "!=", "<", "<=", ">", ">=")) {
return(result)
}
celsius(result)
}
a <- celsius(100)
b <- celsius(37)
print(a)
result <- celsius(100)
result_val <- a$value - b$value
cat("100 C - 37 C =", result_val, "C\n")
cat("100 C > 37 C:", a$value > b$value, "\n")
Internal Generics
Some R functions are internal generics — they perform dispatch inside C code rather than through UseMethod(). Examples include [, [[, $, length, dim, c, and unlist.
# The [ operator is an internal generic
# You can still write methods for it
my_vec <- structure(1:10, class = "LabeledVec", labels = letters[1:10])
`[.LabeledVec` <- function(x, i, ...) {
result <- unclass(x)[i]
labels <- attr(x, "labels")[i]
structure(result, class = "LabeledVec", labels = labels)
}
print.LabeledVec <- function(x, ...) {
labels <- attr(x, "labels")
for (j in seq_along(x)) {
cat(labels[j], "=", unclass(x)[j], " ")
}
cat("\n")
}
print(my_vec)
print(my_vec[3:5])
Internal generics are tricky because they don't always follow the exact same dispatch rules as UseMethod(). Some check the class attribute directly in C code.
Inspecting Dispatch with sloop
The sloop package (if available) or manual inspection can help you understand what R dispatches to.
# Manual dispatch inspection
find_method <- function(generic, class_vec) {
for (cls in class_vec) {
method_name <- paste0(generic, ".", cls)
if (exists(method_name, mode = "function")) {
return(method_name)
}
}
default_name <- paste0(generic, ".default")
if (exists(default_name, mode = "function")) {
return(default_name)
}
return("No method found")
}
# Test it
greet <- function(x, ...) UseMethod("greet")
greet.Person <- function(x, ...) cat("Hello,", x$name, "\n")
greet.default <- function(x, ...) cat("Hello, stranger\n")
obj <- structure(list(name = "Alice"), class = c("Student", "Person"))
cat("Dispatches to:", find_method("greet", class(obj)), "\n")
greet(obj)
# Check which methods exist for a generic
cat("\nAll greet methods:\n")
print(methods("greet"))
Summary Table
Concept
Description
Key Function
Generic function
Function that dispatches based on class
Contains UseMethod()
Method
Implementation for a specific class
Named generic.class()
Default method
Fallback when no class matches
Named generic.default()
Dispatch order
Left to right through class vector
class(x) determines order
NextMethod()
Continue to next class in chain
Called inside a method
Group generics
One method covers many operators
Ops, Math, Summary, Complex
Internal generics
Dispatch in C code
[, [[, c, length, etc.
Practice Exercises
Exercise 1: Create a class hierarchy Vehicle > Car > ElectricCar. Define a describe() generic where each level adds information and calls NextMethod().
Q: What happens if UseMethod() finds no matching method and no default? R throws an error: "no applicable method for 'generic' applied to an object of class 'class'". Always provide a generic.default method to handle unexpected classes gracefully.
Q: Can I call UseMethod() with a different class than the first argument's class? Yes. UseMethod("generic", object) lets you dispatch on a different object. However, this is rarely used and can be confusing. Stick to dispatching on the first argument.
Q: Does NextMethod() work with group generics? Yes. Inside an Ops.MyClass method, calling NextMethod() will look for the next class's Ops method or the specific operator method (e.g., +.ParentClass).
What's Next
S4 Classes in R -- Learn formal class definitions with setClass() and typed slots