S3 classes and generic functions

R · Advanced · 14 min

print, summary, plot and predict all work through S3, and most packages define S3 classes.

S3 is R's original and most widely used object system. An S3 object is an ordinary object (usually a list) with a class attribute, a character vector of class names from most to least specific.

A generic function calls UseMethod("name"), which dispatches to the method named name.class for the first class that has one, falling back to name.default. So print(x) on an object of class "temperature" calls print.temperature(x) if it exists. Inside a method, NextMethod() calls the next method in the class vector, the S3 version of super.

To create a class, write a constructor that builds the underlying object and sets the class with structure(list(...), class = "myclass"), plus a validator for invariants. Then add methods for the generics you need: print, format, summary, arithmetic via the Ops group generic, or your own generics.

inherits(x, "myclass") tests class membership; unclass(x) strips the class to show the raw structure. methods(class = "lm") lists methods for a class.

S3 has no formal definitions or checks: anything can claim any class. That makes it flexible and fast to use, but discipline (constructors, validators) matters. Data frames, factors, Dates and lm fits are all S3 objects.

Example

temperature <- function(celsius) {
  stopifnot(is.numeric(celsius))
  structure(list(c = celsius), class = c("temperature", "measure"))
}
print.temperature <- function(x, ...) {
  cat(sprintf("%.1f°C\n", x$c))
  invisible(x)
}
to_f <- function(x, ...) UseMethod("to_f")
to_f.temperature <- function(x, ...) x$c * 9 / 5 + 32
to_f.default <- function(x, ...) stop("don't know how to convert ", class(x)[1])
summary.measure <- function(object, ...) cat("a measure of class", class(object)[1], "\n")

t1 <- temperature(21.5)
print(t1)                         #> 21.5°C
t1                                # auto-print also dispatches
print(to_f(t1))                   #> [1] 70.7
summary(t1)                       #> a measure of class temperature
print(inherits(t1, "measure"))    #> [1] TRUE
print(tryCatch(to_f(1), error = conditionMessage))
#> [1] "don't know how to convert numeric"

Remember

  • An S3 object is a base object with a class attribute
  • UseMethod dispatches to generic.class, then generic.default
  • Write a constructor; S3 itself enforces nothing
  • NextMethod() delegates to the next class

Watch out for

  • Naming ordinary functions with dots like print.data by accident, which turns them into methods.

Check yourself

  1. x has class c("b", "a"). print(x) is called and only print.a and print.default exist. Which runs?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: cran.r-project.org/doc/manuals/r-release/R-lang.html#Object_002doriented-programming