S3 Classes in R
S3 is R’s oldest and simplest object-oriented system. It underlies much of R’s base functionality, including how print(), summary(), and predict() work for different data types. Understanding S3 gives you control over how your own functions behave with different object types.
What Makes Something an S3 Object
An S3 object is simply a base R object with a class attribute. The class attribute is a character vector. You can attach a class to any object using the class() function.
# Create a regular list
person <- list(name = "Alice", age = 30)
class(person)
# [1] "list"
# Add an S3 class
class(person) <- "person"
class(person)
# [1] "person"
The object itself does not change. It is still a list with two elements. The class attribute is just metadata that R uses to decide which function to call.
Creating an S3 Class
A constructor function makes creating class instances consistent. It wraps object creation and lets you validate input.
# Constructor for a person class
person <- function(name, age) {
if (!is.character(name)) {
stop("name must be a character vector")
}
if (!is.numeric(age) || age < 0) {
stop("age must be a non-negative number")
}
structure(
list(name = name, age = age),
class = "person"
)
}
# Create an instance
p <- person("Alice", 30)
p
# $name
# [1] "Alice"
# $age
# [1] 30
# attr(,"class")
# [1] "person"
The structure() function attaches the class while preserving the list contents.
Generic Functions and Method Dispatch
Generic functions are the heart of S3. A generic is a function that delegates to a specific implementation based on the object is class. You create a generic using UseMethod.
# Define a generic for describing people
describe <- function(x) {
UseMethod("describe")
}
# Default method for any class without specific implementation
describe.default <- function(x) {
paste("Something about", class(x)[1])
}
# Person-specific method
describe.person <- function(x) {
paste(x$name, "is", x$age, "years old")
}
# Test it
describe(p)
# [1] "Alice is 30 years old"
# Works with other types too
describe(1:5)
# [1] "Something about integer"
When you call describe(p), R looks for describe.person. If it does not exist, R falls back to describe.default. The naming convention is critical: generic dot class.
Method Dispatch in Detail
R dispatches methods based on the first argument’s class. The search order matters.
- Look for generic.class - specific method
- Look for generic.default - fallback
- Throw an error if neither exists
# Create a subclass: student inherits from person
student <- function(name, age, major) {
structure(
list(name = name, age = age, major = major),
class = c("student", "person")
)
}
s <- student("Bob", 20, "Physics")
# R looks for describe.student first
# Not found, so falls back to describe.person
describe(s)
# [1] "Bob is 20 years old"
The class vector defines inheritance. “student” comes first, so R checks for a student method before looking for person.
NextMethod for Inheritance
When you have class inheritance, NextMethod lets you call the parent class’s implementation. This avoids duplicating code.
# Extend describe for students
describe.student <- function(x) {
# Call the person method first
base_description <- NextMethod()
# Add student-specific info
paste(base_description, "studying", x$major)
}
describe(s)
# [1] "Bob is 20 years old studying Physics"
The base description comes from describe.person, then we append the major.
Common Generics and Extending Them
Many base R functions are S3 generics. You can add methods for your classes to customize behavior.
# Customize print behavior
print.person <- function(x, ...) {
cat("Person: \n")
cat(" Name:", x$name, "\n")
cat(" Age:", x$age, "\n")
invisible(x)
}
p
# Person:
# Name: Alice
# Age: 30
Key generics you will commonly extend:
- print - how objects display
- summary - compact representation
- plot - visualization
- predict - model predictions
- coef - extract coefficients
Checking Class
Use inherits to check if an object inherits from a class.
inherits(p, "person")
# [1] TRUE
inherits(p, "student")
# [1] FALSE
inherits(s, "person")
# [1] TRUE
This is useful inside methods when you need to handle different cases.
Why Use S3
S3 is lightweight and flexible. You do not need to define formal classes or methods. The system is duck-typed: if an object has the right components, it works.
Base R uses S3 extensively. The data.frame class, lm model objects, and time series all rely on S3 methods. Understanding S3 lets you work with these objects more effectively and extend them for your own needs.
Common Pitfalls
Forgetting to call UseMethod is the most common mistake. A generic must call UseMethod or the dispatch never happens.
# Wrong - this will not dispatch
describe <- function(x) {
describe.person(x) # Always calls person method
}
# Right - this dispatches correctly
describe <- function(x) {
UseMethod("describe")
}
Another pitfall: forgetting that class returns a vector. Use class(x)[1] to get the first or primary class.
Conclusion
S3 provides a simple way to add object-oriented behavior to R code. Attach a class attribute to create objects, use UseMethod to create generics, and define generic.class methods for specific behaviors. The system is pragmatic: it favors convention over complexity and gets out of your way.
This foundation makes it easier to understand S4 and R6 when you need more formal OOP structures.