S3 Methods in R — From Zero to
Expert
A complete, hands-on video tutorial
(with code & slides)
What is S3? (the 60‑second gist)
• Object = regular R object + class attribute (e.g., class(x) <- "person")
• Generic = function that dispatches: UseMethod("generic")
• Method = function named [Link] (e.g., [Link])
• S3 is dynamic and informal
– No formal class schema
– Easy to extend at runtime
Minimal example: a custom
generic + method
greet <- function(x, ...) UseMethod("greet")
[Link] <- function(x, ...) "Hello!"
[Link] <- function(x, ...) {
sprintf("Hello, %s (%d)!", x$name, x$age)
}
p <- list(name = "Alice", age = 34); class(p) <- "person"
greet(p) # -> "Hello, Alice (34)!"
greet(1) # -> "Hello!"
How dispatch works
• S3 usually dispatches on the class of the first argument
• Multiple inheritance via class(x) <- c("child","parent")
– Lookup order: [Link] → [Link] →
[Link]
• Helpful tools
– methods("greet")
– utils::getS3method("greet","person")
Constructor + print + summary
methods
new_person <- function(name, age) {
stopifnot([Link](name), length(name) == 1,
[Link](age), length(age) == 1, age >= 0)
structure(list(name = name, age = [Link](age)), class =
"person")
}
[Link] <- function(x, ...) {
cat(sprintf("person{name=%s, age=%d}\n", x$name, x$age))
invisible(x)
}
[Link] <- function(object, ...) {
out <- list(name = object$name, is_adult = object$age >=
18L)
class(out) <- "[Link]"
out
}
[Link] <- function(x, ...) {
cat("Summary(person)\n name:", x$name, "\n isAdult:",
Multiple inheritance +
NextMethod()
new_employee <- function(name, age, title) {
x <- list(name = name, age = [Link](age), title = title)
class(x) <- c("employee", "person")
x
}
[Link] <- function(x, ...) {
cat(sprintf("employee<title=%s>\n", x$title))
NextMethod() # calls [Link]
}
Extending base generics
(format/[Link]/plot)
[Link] <- function(x, ...) sprintf("%s (%d)",
x$name, x$age)
[Link] <- function(x, ...) sprintf("Person<%s,
%d>", x$name, x$age)
[Link] <- function(x, ...) {
plot(x$age, 1, xlab = "age", ylab = "", yaxt = "n", main =
x$name, ...)
points(x$age, 1, pch = 19)
}
Group generics: Ops / Math /
Summary
new_numwrap <- function(x) structure(list(x = [Link](x)),
class = "numwrap")
[Link] <- function(e1, e2) {
op <- .Generic
if (inherits(e1, "numwrap") && inherits(e2, "numwrap"))
return(new_numwrap([Link](op, list(e1$x, e2%x))))
if (inherits(e1, "numwrap") && [Link](e2))
return(new_numwrap([Link](op, list(e1$x, e2))))
if ([Link](e1) && inherits(e2, "numwrap"))
return(new_numwrap([Link](op, list(e1, e2$x))))
stop(sprintf("Operation '%s' not supported for these
types.", op))
}
[Link] <- function(x, ...) {
op <- .Generic
new_numwrap([Link](op, list(x$x)))
}
[Link] <- function(..., [Link] = FALSE) {
Inspecting & debugging S3
methods
• List methods for a generic
– methods("print"), methods("summary")
• Find a specific method object
– utils::getS3method("summary","lm")
• See where it’s defined
– getAnywhere("[Link]")
• Debug/trace without editing
– debugonce([Link])
– trace("[Link]", tracer = quote(cat("->
entering\n")))
Packaging & registration (roxygen2)
#' @export
greet <- function(x, ...) UseMethod("greet")
#' @exportS3Method greet person
[Link] <- function(x, ...) sprintf("Hi, %s!", x$name)
#' @exportS3Method print person
[Link] <- function(x, ...) { cat(x$name, "\n");
invisible(x) }
CDISC-flavored example: adsl_tbl
wrapper
new_adsl_tbl <- function(df, studyid) {
stopifnot([Link](df))
structure(df, class = c("adsl_tbl", class(df)), studyid =
studyid)
}
print.adsl_tbl <- function(x, ...) {
cat("adsl_tbl<studyid=", attr(x, "studyid"), ">\n", sep =
"")
cat("Rows:", nrow(x), " Cols:", ncol(x), "\n")
if ("TRT01P" %in% names(x)) {
cat("TRT01P counts:\n"); print(table(x$TRT01P, useNA =
"ifany"))
}
invisible(x)
}
summary.adsl_tbl <- function(object, ...) {
out <- list(
studyid = attr(object, "studyid"),
n = nrow(object),
Best practices & common pitfalls
• Name methods exactly [Link]
– Avoid typos & mismatched signatures
• Keep methods compatible with the generic
– Include ... for forward compatibility
• Don’t redefine base generics
– Only implement methods for them
• Prefer inherits(x, "cls") over class(x) == "cls"
– It respects multiple inheritance
• When packaging
– Export the generic; register methods
(@exportS3Method)
Live demo plan (recording guide)
• Segment 1 (2 min): Intro & S3 idea
– Show class attribute & UseMethod
• Segment 2 (4 min): Constructor/print/summary
– Create and inspect person objects in RStudio
• Segment 3 (4 min): Inheritance + NextMethod
– employee extends person
• Segment 4 (5 min): Base & group generics
– [Link], plot, Ops/Math examples
• Segment 5 (3 min): Packaging + pitfalls
– Show roxygen tags; methods() checks
Exercises & next steps
• Implement score() generic + methods for person/employee
• Add [.person to access fields safely (optional)
• Wrap a tibble into adsl_tbl and extend print/summary for your study
• Package it: add roxygen tags, build, install, test with methods()