R OOP with S3
S3 is R's simplest and most widely used object-oriented system. It lets you create custom object types and define how standard R functions behave differently for each type. S3 powers how print(), summary(), and plot() produce different output depending on the object passed to them.
What Is S3?
When you call print(x): Is x a data frame? → print.data.frame() Is x a matrix? → print.matrix() Is x a factor? → print.factor() None match? → print.default() This automatic routing based on class is S3 dispatch.
Creating an S3 Object
# Method 1: assign class after creation student <- list(name="Asha", age=22, score=88) class(student) <- "Student" # Method 2: use structure() student <- structure( list(name="Asha", age=22, score=88), class="Student" ) # Check class(student) # "Student" is.list(student) # TRUE (S3 objects are usually lists under the hood)
Constructor Function
# Wrap object creation in a function for safety
new_student <- function(name, age, score) {
if (!is.character(name)) stop("name must be a character string")
if (score < 0 || score > 100) stop("score must be 0-100")
structure(
list(name=name, age=age, score=score),
class="Student"
)
}
asha <- new_student("Asha", 22, 88)
balu <- new_student("Balu", 25, 72)
S3 Methods — Defining print.Student
print.Student <- function(x, ...) {
cat("=== Student Record ===\n")
cat("Name: ", x$name, "\n")
cat("Age: ", x$age, "\n")
cat("Score:", x$score, "\n")
cat("Grade:", if(x$score >= 75) "Pass" else "Fail", "\n")
invisible(x)
}
print(asha)
# === Student Record ===
# Name: Asha
# Age: 22
# Score: 88
# Grade: Pass
summary.Student Method
summary.Student <- function(object, ...) {
cat("Student:", object$name, "\n")
cat("Percentile estimate:", round(pnorm(object$score, 75, 12)*100), "%\n")
}
summary(asha)
# Student: Asha
# Percentile estimate: 86 %
Generic Functions and Method Dispatch
# See all methods for a generic
methods(print) # lists print.lm, print.data.frame, etc.
methods(summary)
# See all methods for a class
methods(class="Student")
# print.Student summary.Student
# UseMethod() creates a new generic
grade <- function(x, ...) UseMethod("grade")
grade.Student <- function(x, ...) {
if (x$score >= 90) "A"
else if (x$score >= 75) "B"
else if (x$score >= 60) "C"
else "F"
}
grade(asha) # "B"
grade(balu) # "C"
Inheritance with S3
# Graduate student inherits from Student
new_grad_student <- function(name, age, score, thesis) {
obj <- new_student(name, age, score)
obj$thesis <- thesis
class(obj) <- c("GradStudent", "Student") # multiple classes
obj
}
print.GradStudent <- function(x, ...) {
NextMethod() # calls print.Student first
cat("Thesis:", x$thesis, "\n")
}
grad <- new_grad_student("Cena", 26, 92, "ML in Healthcare")
print(grad)
# === Student Record ===
# Name: Cena Age: 26 Score: 92 Grade: Pass
# Thesis: ML in Healthcare
Checking Class and Inheritance
inherits(grad, "Student") # TRUE inherits(grad, "GradStudent") # TRUE inherits(asha, "GradStudent") # FALSE is(grad, "Student") # TRUE
S3 Pros and Cons
Pros: Cons: ────────────────────────────────── ─────────────────────────────────── Simple and flexible No formal validation of structure Works with all R tools Methods can be added by anyone Low overhead No private fields Most common R OOP system Less strict than S4 or R6
S3 is how most R packages implement custom objects. When you call summary(lm_model) and get a regression table instead of a list summary, that is S3 dispatch at work. Building your own S3 classes lets you create well-behaved custom data structures that integrate naturally with R's existing ecosystem.
