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.

Leave a Comment

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