OOP Overview and Base Types
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. Almost everything on this page is base R (the base, methods, utils and stats packages that ship with R). The comparison of the five object systems also runs two CRAN packages, R6 2.6.1 and S7 0.2.2, and the "who uses what" example loads three more: Matrix 1.7-5 (a recommended package that ships with R), ggplot2 4.0.3 and shiny 1.14.0. The lintr 3.4.0 default linters were used only to check the scripts. R has more than one answer to the question "what is this object?". The first answer is its type, the way the interpreter stores it; the second is its class, the label that method dispatch looks at; and on top of those sit five different object systems that use the class label in five different ways. This page separates the vocabularies (typeof(), mode(), storage.mode(), class()), explains the base machinery that every system builds on (the class attribute, inherits(), is.object(), primitives and internal generics) and then compares S3, S4, Reference Classes, R6 and S7 on the same one-field object.
Every example is a script that was run with Rscript on R 4.6.1 with warnings printed as they happen (options(warn = 1)), and the lines that start with #> are what it printed. The scripts also pass the default lintr linters. The names books, shelf_today and the book() constructor come from the running library-shelf scenario described on the section index: a script that needs them starts with source("scenario.R"). Where a message contains quotation marks, they are the quotation marks R printed in the UTF-8 locale the scripts ran in. The details of each system have their own pages: S3 Classes and Generics, S3 Group Generics and Operator Overloading, S4 Classes and Methods, Reference Classes and R6 and The S7 Object System; this page is the map that tells you which one to open.
Objects, types and classes
The R Language Definition starts its chapter on objects with the statement that R gives no direct access to memory: it provides a number of specialised data structures called objects, referred to through symbols, and the symbols are objects too. The function typeof() returns the type of an object. Underneath, every object is a pointer to the same C structure (SEXPREC), and the different R data types are values of the C type SEXPTYPE, which decides how the rest of the structure is used (R-lang: Objects). Four questions about an object are easy to confuse, so it helps to name them once:
| Question | Function | Answer |
|---|---|---|
What is it made of? |
|
One of about two dozen internal types: |
What kind of thing is it, in the language of the S book? |
|
A coarser grouping of the type, kept for compatibility with S: |
What does method dispatch call it? |
|
A character vector: the |
Is it an "object" in the object-oriented sense? |
|
|
Source: R documentation: class
R is deliberately relaxed about the relationship between these questions. A "class" in R is not a declaration that the compiler checks; it is a character vector stored as an attribute (or computed on the fly) and read by whichever generic function happens to be called. The R Language Definition says as much in its chapter on object-oriented programming: R does not have a full-fledged object-oriented system, but a class system and a mechanism for dispatching on the class of an object, and the authors caution users to apply it in a straightforward manner (R-lang: Object-oriented programming). The rest of this page works through what that means, from the bottom (types) to the top (the five systems).
The SEXPTYPE table
R Internals lists the internal types in a table of 24 values: types 0 to 10 and 13 to 25 are in use, and 11 and 12, which were once used for internal factors and ordered factors, have been withdrawn. The numbers are stored in saved objects, so the gap cannot be reused. The table below summarises it and adds what typeof() returns for each type (the strings come from the table in the Language Definition, which also marks the types a user cannot easily get hold of, and from the typeof() help page). R Internals names the first entry NILSXP; it is the NULL object, and the manual does not use the spelling NULLSXP.
| SEXPTYPE | No. | typeof() |
What it is |
|---|---|---|---|
|
0 |
|
|
|
1 |
|
symbols (names) |
|
2 |
|
pairlists; mostly internal, used for argument lists |
|
3 |
|
functions written in R: formals, body, environment |
|
4 |
|
environments |
|
5 |
|
promises, the implementation of lazy evaluation |
|
6 |
|
calls, including formulae |
|
7 |
|
primitive functions that receive their arguments unevaluated |
|
8 |
|
primitive functions that receive evaluated arguments |
|
9 |
|
internal strings, the elements of a character vector (rarely visible) |
|
10 |
|
logical vectors |
|
13 |
|
integer vectors |
|
14 |
|
numeric (double) vectors |
|
15 |
|
complex vectors |
|
16 |
|
character vectors |
|
17 |
|
the dot-dot-dot object |
|
18 |
|
a type that matches all types; there are no objects of this type |
|
19 |
|
lists (generic vectors) |
|
20 |
|
expression vectors, most often the result of |
|
21 |
|
compiled byte code |
|
22 |
|
external pointers |
|
23 |
|
weak references |
|
24 |
|
raw vectors |
|
25 |
|
objects that do not consist solely of a simple type such as a vector or a function; S4 objects are the classical case |
Source: R Internals: SEXPTYPEs
Two points of the table are worth remembering. First, typeof() reports the vector type "double" for what R documentation calls "numeric", and "character" for STRSXP: the SXP names are the C names, and the R-level strings are different. Second, functions come in three types, CLOSXP, SPECIALSXP and BUILTINSXP, which the manual sometimes lumps into a pseudo-type FUNSXP with code 99 when a type is stored in an integer; the last two are the primitives that the section on primitives below explains. The script builds one object for each type that an R program can reach and prints what typeof() says.
library(S7)
# The SEXPTYPEs that an R program can reach, with typeof() of an example.
reachable <- c(
NILSXP = "NULL", SYMSXP = "quote(x)", LISTSXP = "pairlist(a = 1)",
CLOSXP = "function(x) x", ENVSXP = "globalenv()",
LANGSXP = "quote(f(x))", SPECIALSXP = "`if`", BUILTINSXP = "sum",
LGLSXP = "TRUE", INTSXP = "1L", REALSXP = "1", CPLXSXP = "1i",
STRSXP = "'a'", VECSXP = "list(1)", EXPRSXP = "expression(x)",
EXTPTRSXP = "new('externalptr')", RAWSXP = "as.raw(1)",
OBJSXP = "new_class('K')()"
)
data.frame(
sexptype = names(reachable),
example = unname(reachable),
typeof = vapply(
reachable, \(code) typeof(eval(str2lang(code))), character(1L),
USE.NAMES = FALSE
)
)
#> sexptype example typeof
#> 1 NILSXP NULL NULL
#> 2 SYMSXP quote(x) symbol
#> 3 LISTSXP pairlist(a = 1) pairlist
#> 4 CLOSXP function(x) x closure
#> 5 ENVSXP globalenv() environment
#> 6 LANGSXP quote(f(x)) language
#> 7 SPECIALSXP `if` special
#> 8 BUILTINSXP sum builtin
#> 9 LGLSXP TRUE logical
#> 10 INTSXP 1L integer
#> 11 REALSXP 1 double
#> 12 CPLXSXP 1i complex
#> 13 STRSXP 'a' character
#> 14 VECSXP list(1) list
#> 15 EXPRSXP expression(x) expression
#> 16 EXTPTRSXP new('externalptr') externalptr
#> 17 RAWSXP as.raw(1) raw
#> 18 OBJSXP new_class('K')() object
Source: R documentation: typeof
The types that are missing from the output are the ones a user is not meant to hold: CHARSXP, PROMSXP, DOTSXP, ANYSXP, BCODESXP and WEAKREFSXP. Promises appear indirectly (every function argument is one, see Lazy Evaluation and Promises), ... is reachable through helpers such as ...length(), and compiled byte code is hidden behind the closure that owns it. The last row is the newest: the S7 class built in the script is an OBJSXP object, and typeof() returns "object" for it. The section on OBJSXP below returns to that.
typeof(), mode(), storage.mode() and class()
The next script asks all four questions about twenty objects, one per row. The objects are written as code strings and evaluated with str2lang() so that the first column shows what was asked. Two of the objects come from the shelf scenario (shelf_today and the data frame books); the factor, the formula and the matrix stand for classes that base R and stats define.
source("scenario.R")
# One object per row, written as the code that makes it.
codes <- c(
"NULL", "TRUE", "1L", "2.5", "1i", "'a'", "as.raw(1)", "list(1)",
"quote(x)", "quote(x + 1)", "expression(x)", "sum", "`if`", "mean",
"globalenv()", "matrix(1:4, 2)", "factor('a')", "shelf_today", "books",
"y ~ x"
)
describe_object <- function(code) {
x <- eval(str2lang(code))
c(
typeof = typeof(x),
mode = mode(x),
storage.mode = storage.mode(x),
class = paste(class(x), collapse = "/")
)
}
kinds <- t(vapply(codes, describe_object, character(4L)))
noquote(kinds)
#> typeof mode storage.mode class
#> NULL NULL NULL NULL NULL
#> TRUE logical logical logical logical
#> 1L integer numeric integer integer
#> 2.5 double numeric double numeric
#> 1i complex complex complex complex
#> 'a' character character character character
#> as.raw(1) raw raw raw raw
#> list(1) list list list list
#> quote(x) symbol name symbol name
#> quote(x + 1) language call language call
#> expression(x) expression expression expression expression
#> sum builtin function function function
#> `if` special function function function
#> mean closure function function function
#> globalenv() environment environment environment environment
#> matrix(1:4, 2) integer numeric integer matrix/array
#> factor('a') integer numeric integer factor
#> shelf_today double numeric double Date
#> books list list list data.frame
#> y ~ x language call language formula
Source: R documentation: mode
Reading the table row by row shows where the four vocabularies diverge:
-
Integers and doubles share the mode
"numeric"and differ intypeof()andstorage.mode(). In the S language both were "numeric" and the storage mode distinguished them; the manual saysstorage.mode()is generally used when calling code in another language such as C or Fortran, to make sure an R object has the data type the routine expects (R-lang: Objects). -
Functions have one mode,
"function", and three types:"closure"formean,"builtin"forsumand"special"forif. The class of all three is"function". -
Symbols have the type
"symbol"but the mode"name"(and the class"name"); calls have the type"language"and the mode and class"call". The help page adds that some calls have the mode"(", which is S-compatible. -
Attributes change the class but not the type. The matrix is still an integer vector; the factor is still an integer vector with attributes; the
Dateis still a double; the data frame is still a list; and the formula is still a call. This is whytypeof()is the question to ask when you want to know how the object is stored, andclass()the question when you want to know how it will be treated.
mode() and storage.mode() also have replacement forms. mode(x) <- "character" changes the mode through the matching as. function, so it is only supported for modes that have one, and attributes are preserved. storage.mode(x) <- "double" is the more efficient primitive version, which works for the internal types listed under typeof(). The mode "single" is a pseudo-mode: setting it records the attribute "Csingle" and the real storage mode stays "double". The pitfalls section below shows both replacement forms in a script.
The help page for typeof() says the possible values are the entries of a table in the R sources and lists those that a user is likely to see: the vector types, "NULL", "closure", "special", "builtin", "environment" and "S4", plus the rarer "symbol", "pairlist", "promise", "object", "language", "char", "...", "any", "expression", "externalptr", "bytecode" and "weakref". Note that "object" and "S4" are both in the list; the section on OBJSXP shows when each appears.
Implicit class and the dispatch class
An object without a class attribute still has a class: the implicit class. class(x) computes it, and the help page lists the rules. A matrix gets "matrix" and "array", an array gets "array", a function "function", NULL gets "NULL", and other objects get the result of typeof() (which the help page says is similar to mode()), with "integer" and "double" reported as "integer" and "numeric". For calls the class is "call", except that there are extra classes for the special calls if, for, while, (, {, <- and =.
m <- matrix(1:4, 2)
class(m)
#> [1] "matrix" "array"
oldClass(m)
#> NULL
.class2(m)
#> [1] "matrix" "array" "integer" "numeric"
inherits(m, "matrix")
#> [1] TRUE
inherits(m, "integer")
#> [1] FALSE
is.object(m)
#> [1] FALSE
Source: R documentation: class
The matrix has no class attribute (oldClass() is NULL, is.object() is FALSE), yet class(m) has two elements. This is the R 4.0.0 change: the NEWS entry says matrix objects now also inherit from the class "array", so that class(diag(1)) is c("matrix", "array"), and that this invalidates code that wrongly assumed class(matrix_obj) has length one; it adds that S3 methods for class "array" are now dispatched for matrix objects (R NEWS: Changes in R 4.0.0). inherits(m, "matrix") is TRUE, because inherits() works on class(x), but inherits(m, "integer") is FALSE: the implicit class used for dispatch is longer than class() says, and inherits() does not use it.
# The implicit class of other objects without a class attribute.
class(array(1:8, c(2L, 2L, 2L)))
#> [1] "array"
class(1L)
#> [1] "integer"
class(2.5)
#> [1] "numeric"
class(sum)
#> [1] "function"
class(NULL)
#> [1] "NULL"
class(quote(x + 1))
#> [1] "call"
class(quote(x <- 1))
#> [1] "<-"
class(quote(if (a) b))
#> [1] "if"
.class2(1L)
#> [1] "integer" "numeric"
.class2(2.5)
#> [1] "double" "numeric"
.class2(sum)
#> [1] "function"
Source: R documentation: class
The longer vector is .class2(), a function R 4.0.0 added, whose NEWS entry calls it the full character vector of class names used for S3 method dispatch. The help page says it is for didactic, diagnostic or debugging use, and that using it for other reasons may be a misuse. For a matrix of integers it returns "matrix", "array", "integer", "numeric": a method for numeric can serve a matrix of integers when none for matrix, array or integer exists. For an integer it returns "integer", "numeric", and for a double "double", "numeric". The next script writes five methods for one generic and shows which one answers for which object.
# S3 dispatch walks .class2(x), then falls back to the default method.
describe <- function(x) UseMethod("describe")
describe.integer <- function(x) "integer method"
describe.numeric <- function(x) "numeric method"
describe.matrix <- function(x) "matrix method"
describe.function <- function(x) "function method"
describe.default <- function(x) "default method"
describe(1L)
#> [1] "integer method"
describe(2.5)
#> [1] "numeric method"
describe(m)
#> [1] "matrix method"
describe(sum)
#> [1] "function method"
describe("a")
#> [1] "default method"
Source: R documentation: UseMethod
describe(1L) is answered by the integer method and describe(2.5) by the numeric method, because "double" has no method and "numeric" is next in the implicit class; deleting describe.integer would make the integer use the numeric method too. The matrix finds describe.matrix before it finds anything for integer, and a character string, whose implicit class is "character", falls through to describe.default. A note from the help page is worth keeping in mind: UseMethod() dispatches on the class returned by class() with these interpolated implicit classes, whereas group generics dispatch on oldClass() for efficiency, and internal generics dispatch only on objects for which is.object() is true. The details of dispatch are the subject of S3 Classes and Generics.
The class attribute: oldClass(), is.object(), inherits() and unclass()
When an object does carry a class attribute, the attribute is the class. The functions that read and write it are small, and each one answers a different question. The script below uses the book from the shelf scenario, which is a list of six length-one fields with the class "book".
source("scenario.R")
dispossessed <- book(
"The Dispossessed", "Ursula K. Le Guin", "978-0-306-40615-7",
1974, "Science fiction", 387
)
typeof(dispossessed)
#> [1] "list"
mode(dispossessed)
#> [1] "list"
class(dispossessed)
#> [1] "book"
oldClass(dispossessed)
#> [1] "book"
is.object(dispossessed)
#> [1] TRUE
inherits(dispossessed, "book")
#> [1] TRUE
inherits(dispossessed, "list")
#> [1] FALSE
is.list(dispossessed)
#> [1] TRUE
Source: R documentation: class
-
class(x)andoldClass(x)agree for an object with a class attribute. They differ for implicit classes, as the matrix showed:oldClass()returns only the attribute and givesNULLotherwise. Both, together with their replacement forms,.class2(),unclass()andis.object(), are primitive functions;inherits()andisa()are not. -
is.object(x)isTRUEfor thebook, because the class attribute is set, andFALSEfor the matrix and for every plain vector. S4 objects have a class attribute too, so the OBJECT bit is set for them as well (R Internals: S4 objects). The primitive functions that are internal generics look at this bit and do nothing when it is not set, so an object has to be an "object" before the object systems can see it. -
inherits(x, "list")isFALSEeven though the type is"list"andis.list(x)isTRUE.inherits()asks about the class vector, not about the type. Abookis a list in the sense of storage and not in the sense of class, andinherits(x, "list")would beTRUEonly if"list"were in the class vector. -
unclass(x)returns a copy without the class attribute, which is how a method looks at the underlying structure:validate_book()in the scenario starts withunclass(x)for that reason. It is not allowed for objects that cannot be copied, namely environments and external pointers.
# What the type and the class say about the unclassed list.
plain <- unclass(dispossessed)
typeof(plain)
#> [1] "list"
class(plain)
#> [1] "list"
is.object(plain)
#> [1] FALSE
names(plain)
#> [1] "title" "author" "isbn" "year" "genre" "pages"
Source: R documentation: class
After unclass() the object is a plain list: class() returns the implicit class "list", is.object() is FALSE, and the six field names are visible. Nothing about the data changed; only the label did. That is the whole mechanism of S3: objects are ordinary vectors, lists or functions, and the label decides which functions treat them specially.
inherits(), its which argument and isa()
inherits(x, what, which = FALSE) returns TRUE if any of the names in what matches any element of class(x). With which = TRUE it returns an integer vector as long as what, in which each element is the position in class(x) of the matching class and 0 means no match. That is how to find out how far down the inheritance chain a class sits. The sibling isa(x, what) is stricter: it is TRUE only if all elements of what are contained in class(x) (for an S4 object it uses is()).
# inherits() with which = TRUE gives the position in the class vector.
x <- structure(list(), class = c("student", "person"))
inherits(x, c("person", "student", "teacher"), which = TRUE)
#> [1] 2 1 0
inherits(x, c("person", "student", "teacher"))
#> [1] TRUE
inherits(x, "student", which = FALSE)
#> [1] TRUE
isa(x, c("student", "person"))
#> [1] TRUE
isa(x, "person")
#> [1] TRUE
Source: R documentation: class
The first call returns 2 1 0: "person" is the second class of x, "student" the first and "teacher" is not there. The NEWS file contains no change to how which or missing matches behave, so the semantics are the ones in the help page. The one change to inherits() in recent versions is the what argument, described next.
inherits() with a class object: nameOfClass() (R 4.3.0)
R 4.3.0 let what be something other than a character vector. A new generic, nameOfClass(), is called to resolve the class name from what; the NEWS entry says this supports experimenting with alternative object systems (R NEWS: Changes in R 4.3.0). An S7 class object is the practical example: inherits(obj, Class) works with the class object, not only with its name.
library(S7)
book_s7 <- new_class("BookS7", properties = list(title = class_character))
s7_book <- book_s7(title = "Ten Winters")
# R 4.3.0: inherits() accepts a non-character `what` through nameOfClass().
inherits(s7_book, "BookS7")
#> [1] TRUE
inherits(s7_book, book_s7)
#> [1] TRUE
nameOfClass(book_s7)
#> [1] "BookS7"
nameOfClass("BookS7")
#> NULL
# A class object of your own can take part by defining the method.
shelf_class <- structure(list(name = "book"), class = "shelf_class")
nameOfClass.shelf_class <- function(x) x$name
inherits(structure(list(), class = "book"), shelf_class)
#> [1] TRUE
Source: R documentation: class
The S7 package defines a nameOfClass() method for its class objects, so inherits(s7_book, book_s7) is TRUE; the generic returns NULL when it has no method for its argument, and inherits() then reports an error for a what that is neither a character vector nor an object with such a method. The last call shows that a class object of your own can take part by defining one S3 method. The same release added two other hooks, chooseOpsMethod() for resolving two competing methods of an Ops group generic and the S3 generic @ operator; they are covered in S3 Group Generics and Operator Overloading and The S7 Object System.
Assigning a class
class(x) <- value and oldClass(x) <- value set the attribute; assigning NULL or an empty character vector removes it. Assigning a class does not check anything about the data, with a few exceptions that R enforces because the class would otherwise lie about the structure.
x <- 1:6
class(x) <- "tally"
class(x)
#> [1] "tally"
typeof(x)
#> [1] "integer"
is.object(x)
#> [1] TRUE
inherits(x, "integer")
#> [1] FALSE
is.integer(x)
#> [1] TRUE
# Without a method, the class attribute is only printed as an attribute.
x
#> [1] 1 2 3 4 5 6
#> attr(,"class")
#> [1] "tally"
unclass(x)
#> [1] 1 2 3 4 5 6
# Assigning NULL or an empty vector removes the class attribute.
class(x) <- NULL
class(x)
#> [1] "integer"
is.object(x)
#> [1] FALSE
# Some assignments are checked, or change the object.
y <- 1:6
tryCatch(class(y) <- "matrix", error = conditionMessage)
#> [1] "invalid to set the class to matrix unless the dimension attribute is of length 2 (was 0)"
class(y) <- "numeric"
typeof(y)
#> [1] "integer"
z <- NULL
tryCatch(class(z) <- "tally", error = conditionMessage)
#> [1] "attempt to set an attribute on NULL"
Source: R documentation: class
An integer vector with the class "tally" is still an integer (is.integer() is TRUE), but inherits(x, "integer") is FALSE, because its class vector no longer contains "integer"; without a method for "tally", printing falls back to the default and shows the attribute. class(y) <- "matrix" is refused when the dimension attribute does not have length 2, and NULL cannot hold attributes at all. The help page also says that for classes with a formal (S4) definition, directly replacing the class this way is strongly deprecated: as(object, value) is the way to coerce an object to a class.
Primitives and internal functions
Most of the functions that ship with R are written in R. Typing mean shows its source, a one-line call to UseMethod("mean"), and that is what R FAQ 7.40 means when it says that in most cases typing the name of a function prints its source code, with the exception of code that is hidden in a namespace or compiled (R FAQ 7.40). A smaller set of functions are not written in R at all: they are the C code compiled into the interpreter and called directly, and they print as .Primitive("name"). The Language Definition calls them builtin objects and special forms, and notes that, from the R language, they are just another kind of function that is.primitive() can tell apart from interpreted functions (R-lang: Builtin objects and special forms).
sum
#> function (..., na.rm = FALSE) .Primitive("sum")
typeof(sum)
#> [1] "builtin"
is.primitive(sum)
#> [1] TRUE
formals(sum)
#> NULL
body(sum)
#> NULL
environment(sum)
#> NULL
args(sum)
#> function (..., na.rm = FALSE)
#> NULL
sum prints as function (..., na.rm = FALSE) .Primitive("sum") because there is no R code to print: the line is the signature R keeps for documentation and argument matching, followed by the marker. Everything an R function has is absent. formals() and body() return NULL because a primitive has neither, environment() returns NULL because it was not created in an environment, and args() is the supported way to see its interface: it returns a closure with the same formals and a NULL body. is.primitive(x) is defined as typeof(x) being either "builtin" or "special".
Builtin or special
The two types differ in one thing only: when the arguments are evaluated. A builtin has its arguments evaluated and passed to the C code, in accordance with call-by-value; a special receives the unevaluated arguments and decides for itself, which is necessary for the language constructs, the assignment operators, && and ||, quote(), substitute(), missing() and on.exit(). Subsetting and subassignment operators and @ are specials too (for both extraction and replacement forms, $ and @ take a symbol argument, and [ and [[ allow missing arguments). R Internals stresses that being special or builtin is a separate property from being primitive or .Internal: quote is a special primitive, + is a builtin primitive, cbind is a special .Internal and grep is a builtin .Internal (R Internals: Argument evaluation).
`if`
#> .Primitive("if")
typeof(`if`)
#> [1] "special"
typeof(`*`)
#> [1] "builtin"
typeof(`[`)
#> [1] "special"
typeof(quote)
#> [1] "special"
Source: R Internals: Special primitives
Closures for comparison
mean is a closure: it has formals, a body and an environment, and environmentName() shows it lives in the base namespace. A closure can be compiled to byte code (its printed form then shows a bytecode line and an environment line with an address, which this page does not reproduce), but it remains a "closure" to typeof().
# A closure: written in R, with formals, a body and an environment.
typeof(mean)
#> [1] "closure"
is.primitive(mean)
#> [1] FALSE
names(formals(mean))
#> [1] "x" "..."
body(mean)
#> UseMethod("mean")
environmentName(environment(mean))
#> [1] "base"
Source: R documentation: typeof
.Primitive() and .Internal()
Two functions give access to the C table behind all this. .Primitive(name) looks a primitive up by name, and the help page says it is almost never used: the backquoted name, or get(name, envir = baseenv()), does the same and does not depend on knowing which functions are primitive, which changes as R evolves. .Internal(call) performs a call to internal code that is built into the interpreter. Its help page tells readers that only R developers can add to the list of internal functions and that using the function is a job for experts; it is itself a primitive, as the script shows. You cannot set attributes on primitive functions.
# .Primitive() looks a primitive up by name, and .Internal is itself one.
identical(.Primitive("sum"), sum)
#> [1] TRUE
typeof(.Internal)
#> [1] "special"
tryCatch(structure(sum, note = "mine"), error = conditionMessage)
#> [1] "cannot set an attribute on a 'builtin'"
# A closure that wraps .Internal(): the call is visible in the body.
internal_line <- grep(".Internal", deparse(body(grepl)), fixed = TRUE)
trimws(deparse(body(grepl))[internal_line])
#> [1] ".Internal(grepl(as.character(pattern), x, ignore.case,"
R Internals explains the design: C code compiled into R is called either directly, as a primitive, or through the .Internal interface, which is very similar to .External except in syntax. R keeps a table (R_FunTab in the source file src/main/names.c) of R function names and C functions, which says how many arguments each takes, whether they are evaluated before the call, and whether the function is "internal", reachable only through .Internal, or directly accessible, in which case it prints as .Primitive. Functions that use .Internal() wrapped in a closure are generally preferred, because the closure ensures standard handling of named and default arguments and allows R code to run before the C code: grepl() above converts a factor or a number to character in R, then calls .Internal(grepl(...)). Primitives are the exceptions, used for convenience and efficiency, and because some are needed for basic operations (.Internal itself must be a primitive). Primitives make no use of R code, so they differ from ordinary functions in argument matching: positional matching is important for call, switch, .C and .subset, to avoid partial matching of the first argument, and the arithmetic, comparison and logical operators discard argument names when called as functions.
Which functions are primitive is not stable. The manual’s list "is subject to change", and it contains the language elements and operators ({, (, if, for, while, function, [, [[, $, @, the assignment operators and the arithmetic, comparison and logic operators), the one-argument mathematical functions (abs, sqrt, exp, cumsum and friends), the programming utilities (missing, on.exit, as.integer, is.na, is.null, is.object, unclass, invisible, seq_along) and the basic accessors and replacement functions (length, class, oldClass, attr, names, dim, levels<-, storage.mode<-). Replacement functions are primitive where possible to avoid copying. A few primitives exist for efficiency only: c, list, rep, seq.int, round, signif, UseMethod and standardGeneric among them.
Writing R Extensions adds a rule for package authors. It says it is not portable to call compiled code in R or in other packages through .Internal, .C, .Fortran, .Call or .External, because such interfaces are subject to change without notice and will probably result in code that terminates the R process (Writing R Extensions: Writing portable packages). .Internal is for the R sources; user code and package code call the closures that wrap it, and use .Call() with their own registered routines for their own compiled code (see Interfacing C, C++ and Fortran).
Prototypes for primitives
Because a primitive has no formals, R keeps prototypes for printing, for args() and for package checking (tools::checkS3methods() and the codetools package use them). They live in two environments in the base package, .GenericArgsEnv for the primitives that are internal S3 generics and .ArgsEnv for the rest, and hold closures with the primitives' names, formals derived by hand from the help pages, and a body that is either a suitable call to UseMethod() or NULL (R Internals: Prototypes for primitives).
# Printing and args() use prototypes kept in two environments of base.
exists("sum", envir = .GenericArgsEnv, inherits = FALSE)
#> [1] TRUE
exists("sum", envir = .ArgsEnv, inherits = FALSE)
#> [1] FALSE
exists("quote", envir = .ArgsEnv, inherits = FALSE)
#> [1] TRUE
typeof(.GenericArgsEnv$sum)
#> [1] "closure"
body(.GenericArgsEnv$sum)
#> UseMethod("sum")
sum has a prototype in .GenericArgsEnv, whose body is UseMethod("sum"): it is an internal generic (a member of the Summary group), and the prototype records that. quote is in .ArgsEnv, the environment for primitives that are not generic. The prototypes are what make the function (..., na.rm = FALSE) part of the printed line; the primitive itself is the C function behind it.
Internal generics: dispatch on a primitive
Primitives are not generic functions in the sense of UseMethod("mean"), because there is no R code to hold the call. Yet length, [, c, dim, as.character and many others take methods. The help page InternalMethods lists the functions that are generic in this way: the subsetting operators ([, [[, $ and their replacement forms), length, lengths, dim, dimnames, names and their replacement forms, levels<-, @, c, unlist, cbind, rbind, the as. family, is.na, anyNA, is.numeric, nchar, rep and seq.int, xtfrm and others, plus all the members of the group generics. The vector .S3PrimitiveGenerics lists the primitives that are internal generic and not group generic, and .internalGenerics those reached through .Internal.
How the dispatch works is described in R Internals under "Argument evaluation". A special primitive that is an internal generic does not evaluate its arguments on call; the C code begins with a call to DispatchOrEval, which evaluates the first argument, looks for a method based on its class (if S4 dispatch is on, S4 methods are looked for first, even for S3 classes) and, if it finds one, dispatches to it with the remaining arguments as promises. If no method is found, the remaining arguments are evaluated and control returns to the internal generic. Group generics work another way: most are builtins and their C code calls DispatchGeneric. The efficiency rule is in the help page: for efficiency, internal dispatch only occurs on objects, that is, those for which is.object() returns TRUE.
source("scenario.R")
dispossessed <- book(
"The Dispossessed", "Ursula K. Le Guin", "978-0-306-40615-7",
1974, "Science fiction", 387
)
# length() is a primitive and an internal generic: with no method it counts
# the six fields of the underlying list.
length(dispossessed)
#> [1] 6
length.book <- function(x) 1L
length(dispossessed)
#> [1] 1
length(unclass(dispossessed))
#> [1] 6
Source: R documentation: InternalMethods
length(dispossessed) counts the six fields of the list. After length.book is defined, the same call returns 1: the primitive found the class attribute and dispatched. length(unclass(dispossessed)) is 6 again, since the object is no longer an object and the primitive does not even look for a method. The next fragment of the same script makes the point with a plain list that happens to have a method defined for a class name it does not carry.
# The same dispatch works for the other internal generics, and only for
# objects: a plain list with the same fields never reaches the method.
fields <- list(title = "x", author = "y")
length.fields <- function(x) 99L
length(fields)
#> [1] 2
length(structure(fields, class = "fields"))
#> [1] 99
# Which names are internal generics (and not group generics)?
c("length", "c", "dim", "mean") %in% .S3PrimitiveGenerics
#> [1] TRUE TRUE TRUE FALSE
Source: R Internals: Argument evaluation
The plain list fields is not an object, so length.fields is never consulted and the result is 2; the classed copy dispatches and returns 99. The membership test shows that length, c and dim are in .S3PrimitiveGenerics while mean is not, since mean is an ordinary closure and a generic by the UseMethod() route. In practice this means two things when you write a class. A method for an internal generic such as length or [ takes effect only when the object has a class attribute, and every call on a classed object pays the (small) cost of looking for a method. And a package that defines such a method must register it for dispatch, which the S3 Classes and Generics page shows with S3method() directives in NAMESPACE.
A short history of R’s object systems
The five systems did not arrive together, and their dates explain several of their quirks. The R FAQ dates the first two: Chambers and Hastie’s "Statistical Models in S" (1992) introduced S version 3, which added structures to facilitate statistical modelling, and Chambers’s "Programming with Data" (1998) described version 4 of S, a major revision (R FAQ 3.1). R’s S3 classes follow the first, and the methods package, which was added in R 1.4.0, implements the second. The NEWS of the later releases supply the remaining dates; the two CRAN packages are dated from the CRAN archive.
Source: R FAQ 3.1: What is S?
The dates come from these places:
-
S3 (1992) and S4 (1998) are the years of the S books that R FAQ 3.1 describes.
-
R 1.4.0 (released 2001-12-19) is the release whose NEWS says that a package
methodshas been added, containing formal classes and methods ("S4" methods) that implement the description in "Programming with Data", and that the@operator has been added to the grammar (R NEWS: Changes in R 1.4.0, NEWS.1; the release date is from the source archive). -
R 2.12.0 (released 2010-10-15) is the release whose NEWS says that a facility for defining reference-based S4 classes, in the object-oriented style of Java and C++, has been added experimentally to package
methods(R NEWS: Changes in R 2.12.0, NEWS.2; release date from the source archive). -
R6’s first release in the CRAN archive is 1.0.1, dated 2014-07-17; the current release is 2.6.1. S7 0.1.0 was published on 2023-08-24, and the current release, 0.2.2, on 2026-04-22 (CRAN: S7).
-
R 4.4.0 (released 2024-04-24) is the release whose NEWS records the renaming of the internal type S4SXP to OBJSXP; see the section on OBJSXP.
The same object in five systems
The five systems are easiest to compare on one tiny object, a book with a single field, title. The script builds it in each system and asks the questions of the first half of this page about the result: what type is it, what does class() say, is it an "object" and is it an S4 object?
source("scenario.R")
library(R6)
library(S7)
# The same one-field object in each of the five systems.
s3_book <- book("Ten Winters")
book_s4 <- setClass("BookS4", slots = c(title = "character"))
s4_book <- book_s4(title = "Ten Winters")
book_rc <- setRefClass("BookRC", fields = list(title = "character"))
rc_book <- book_rc$new(title = "Ten Winters")
book_r6 <- R6Class("BookR6", public = list(title = NULL))
r6_book <- book_r6$new()
r6_book$title <- "Ten Winters"
book_s7 <- new_class("BookS7", properties = list(title = class_character))
s7_book <- book_s7(title = "Ten Winters")
objects <- list(
S3 = s3_book, S4 = s4_book, RC = rc_book, R6 = r6_book, S7 = s7_book
)
data.frame(
system = names(objects),
typeof = vapply(objects, typeof, character(1L)),
class = vapply(objects, \(x) class(x)[[1L]], character(1L)),
is_object = vapply(objects, is.object, logical(1L)),
is_s4 = vapply(objects, isS4, logical(1L)),
row.names = NULL
)
#> system typeof class is_object is_s4
#> 1 S3 list book TRUE FALSE
#> 2 S4 S4 BookS4 TRUE TRUE
#> 3 RC S4 BookRC TRUE TRUE
#> 4 R6 environment BookR6 TRUE FALSE
#> 5 S7 object BookS7 TRUE FALSE
Source: R documentation: isS4
The book() from the scenario is an S3 object, a list with a class attribute. setClass() returns a generator function, and its objects have the type "S4"; setRefClass() objects have the same type, because a Reference Class object is an S4 object that holds an environment. An R6 object is an environment with a class vector, so typeof() says "environment". An S7 object has the type "object": it is an OBJSXP object whose S4 bit is not set. All five are objects for is.object(), and only the S4 and Reference Class objects answer TRUE to isS4().
# R6 and S7 objects carry S3 class vectors.
class(r6_book)
#> [1] "BookR6" "R6"
class(s7_book)
#> [1] "BookS7" "S7_object"
inherits(s7_book, "S7_object")
#> [1] TRUE
inherits(r6_book, "R6")
#> [1] TRUE
# unclass() cannot strip the class of an environment.
tryCatch(unclass(r6_book), error = conditionMessage)
#> [1] "cannot unclass an environment"
Source: R documentation: class
R6 and S7 objects have ordinary S3 class vectors, which is how S3 generics such as print() and format() can see them, and which is why inherits(s7_book, "S7_object") works. The last call is a consequence of the R6 type: unclass() is not allowed for environments, so there is no plain-structure view of an R6 object as there is for a book.
Copy-on-modify or reference semantics
R’s usual behaviour is that a function that modifies its argument modifies a copy. S3, S4 and S7 objects behave this way. Reference Classes and R6 objects are environments: a function that receives one and changes a field changes the object everywhere. The script gives each object to a function that sets the title to "Changed" and then looks at the caller’s copy.
source("scenario.R")
library(R6)
library(S7)
s3_book <- book("Ten Winters")
book_s4 <- setClass("BookS4", slots = c(title = "character"))
s4_book <- book_s4(title = "Ten Winters")
book_rc <- setRefClass("BookRC", fields = list(title = "character"))
rc_book <- book_rc$new(title = "Ten Winters")
book_r6 <- R6Class("BookR6", public = list(title = NULL))
r6_book <- book_r6$new()
r6_book$title <- "Ten Winters"
book_s7 <- new_class("BookS7", properties = list(title = class_character))
s7_book <- book_s7(title = "Ten Winters")
# Each function changes the title of the object it receives.
retitle_dollar <- function(x) {
x$title <- "Changed"
invisible(x)
}
retitle_at <- function(x) {
x@title <- "Changed"
invisible(x)
}
retitle_dollar(s3_book)
retitle_at(s4_book)
retitle_at(s7_book)
retitle_dollar(rc_book)
retitle_dollar(r6_book)
# Copy-on-modify (S3, S4, S7): the caller still sees the old title.
s3_book$title
#> [1] "Ten Winters"
s4_book@title
#> [1] "Ten Winters"
s7_book@title
#> [1] "Ten Winters"
# Reference semantics (RC, R6): the caller sees the change.
rc_book$title
#> [1] "Changed"
r6_book$title
#> [1] "Changed"
# An RC object is copied only on request.
rc_copy <- rc_book$copy()
rc_copy$title <- "Copy"
rc_book$title
#> [1] "Changed"
Source: R documentation: refClass
The @ form is the S4 and S7 accessor; the $ form is the list element of the S3 book and the field of the Reference Class and R6 objects. After the calls, the S3, S4 and S7 titles are unchanged (each function modified its own copy), while the Reference Class and R6 titles are "Changed". A Reference Class object is copied only on request with its $copy() method; R6 has the analogous $clone() method. Reference semantics are the reason to pick these two systems and also the reason to be careful with them: a change made through one name is visible through every other name, and the usual rules of Assignment, Names and Values about copies no longer describe such an object. Memory and Copy Semantics covers when R copies.
Generic functions or methods that belong to the class
S3, S4 and S7 are generic-function systems: a method is a function that is registered with a generic, and a call such as shout(x) looks at the class of x to decide which one runs. Reference Classes and R6 are message-passing systems, in the style of Java and Python: the methods are stored in the class, and a call such as x$shout() finds the method through the object. The same operation in each system:
source("scenario.R")
library(R6)
library(S7)
# S3: a generic function that dispatches on the class attribute.
shout <- function(x) UseMethod("shout")
shout.book <- function(x) toupper(x$title)
shout(book("Ten Winters"))
#> [1] "TEN WINTERS"
# S4: a generic declared with setGeneric(), methods registered by class.
book_s4 <- setClass("BookS4", slots = c(title = "character"))
setGeneric("shout4", function(x) standardGeneric("shout4"))
#> [1] "shout4"
setMethod("shout4", "BookS4", function(x) toupper(x@title))
shout4(book_s4(title = "Ten Winters"))
#> [1] "TEN WINTERS"
# Reference classes: methods live inside the class and use fields directly.
book_rc <- setRefClass(
"BookRC",
fields = list(title = "character"),
methods = list(shout = function() toupper(title))
)
book_rc$new(title = "Ten Winters")$shout()
#> [1] "TEN WINTERS"
# R6: methods live inside the class and reach the object through self.
book_r6 <- R6Class(
"BookR6",
public = list(
title = NULL,
initialize = function(title) self$title <- title,
shout = function() toupper(self$title)
)
)
book_r6$new("Ten Winters")$shout()
#> [1] "TEN WINTERS"
# S7: new_generic() and method() bind a function to a class.
book_s7 <- new_class("BookS7", properties = list(title = class_character))
shout7 <- new_generic("shout7", "x")
method(shout7, book_s7) <- function(x) toupper(x@title)
shout7(book_s7(title = "Ten Winters"))
#> [1] "TEN WINTERS"
Source: R documentation: setGeneric
In the S3 version the generic is a plain function that calls UseMethod() and the method is a plain function named generic.class. In S4 the generic is declared with setGeneric() (which prints its name, the #> [1] "shout4" line) and each method is registered with setMethod(). A Reference Class method refers to the fields by name, as in toupper(title), and R6 methods reach the object through self. S7 binds a function to a generic and a class with method(generic, class) <- function. In every case the user-visible effect is the same, and the differences show up in how each system handles inheritance, validation and mutation.
Single or multiple dispatch
S3 dispatches on the class of the first argument only: the second argument is ignored when selecting the method. S4 and S7 can dispatch on several arguments, so a method can be written for a particular combination of classes.
library(S7)
# S3 looks only at the class of the first argument: the second is ignored.
pair_kind <- function(x, y) UseMethod("pair_kind")
pair_kind.numeric <- function(x, y) "first argument is a number"
pair_kind.character <- function(x, y) "first argument is text"
pair_kind(1, "a")
#> [1] "first argument is a number"
pair_kind(1, TRUE)
#> [1] "first argument is a number"
# S4 can look at the classes of several arguments.
setGeneric("pair_kind4", function(x, y) standardGeneric("pair_kind4"))
#> [1] "pair_kind4"
setMethod(
"pair_kind4", signature("numeric", "character"),
function(x, y) "number then text"
)
setMethod(
"pair_kind4", signature("character", "numeric"),
function(x, y) "text then number"
)
pair_kind4(1, "a")
#> [1] "number then text"
pair_kind4("a", 1)
#> [1] "text then number"
# S7 chooses the dispatch arguments when it creates the generic.
pair_kind7 <- new_generic("pair_kind7", c("x", "y"))
method(pair_kind7, list(class_double, class_character)) <-
function(x, y) "number then text"
method(pair_kind7, list(class_character, class_double)) <-
function(x, y) "text then number"
pair_kind7(1, "a")
#> [1] "number then text"
pair_kind7("a", 1)
#> [1] "text then number"
Source: R documentation: Methods_Details
pair_kind(1, "a") and pair_kind(1, TRUE) give the same answer, because only the first argument decides. The S4 and S7 generics select different methods for the same two values in a different order, because both arguments take part. The S4 signature() lists the classes in argument order; S7 declares the dispatch arguments (c("x", "y")) when it creates the generic and takes a list of classes in the method<- call. S3’s UseMethod() can be extended by manual checks inside the method, and Ops group generics have their own double-dispatch rule (see S3 Group Generics and Operator Overloading), but the general rule for S3 is first argument only.
Validation
Nothing in S3 stops you from putting a number where a title belongs: a class is a label, and checks are the constructor’s and the validator’s job. R6 fields are likewise unchecked unless you write the check. S4 slots, Reference Class fields and S7 properties declare a type, and the type is enforced on every assignment.
source("scenario.R")
library(R6)
library(S7)
s3_book <- book("Ten Winters")
book_s4 <- setClass("BookS4", slots = c(title = "character"))
s4_book <- book_s4(title = "Ten Winters")
book_rc <- setRefClass("BookRC", fields = list(title = "character"))
rc_book <- book_rc$new(title = "Ten Winters")
book_r6 <- R6Class("BookR6", public = list(title = NULL))
r6_book <- book_r6$new()
book_s7 <- new_class("BookS7", properties = list(title = class_character))
s7_book <- book_s7(title = "Ten Winters")
# S3 and R6 accept a number where a title belongs.
s3_book$title <- 42
s3_book$title
#> [1] 42
r6_book$title <- 42
r6_book$title
#> [1] 42
# S4, Reference Classes and S7 check the type on assignment.
tryCatch(s4_book@title <- 42, error = conditionMessage)
#> [1] "assignment of an object of class “numeric” is not valid for @‘title’ in an object of class “BookS4”; is(value, \"character\") is not TRUE"
tryCatch(rc_book$title <- 42, error = conditionMessage)
#> [1] "invalid assignment for reference class field ‘title’, should be from class “character” or a subclass (was class “numeric”)"
tryCatch(s7_book@title <- 42, error = conditionMessage)
#> [1] "<BookS7>@title must be <character>, not <double>"
Source: R documentation: validObject
The messages are the real ones from R 4.6.1, S7 0.2.2 and the methods package; the S4 and Reference Class messages use typographic quotation marks. S4 goes further than type checks: validity functions and validObject() enforce rules that involve several slots (see S4 Classes and Methods), and S7 has validators for the same purpose (see The S7 Object System). The book() constructor in the scenario does its checking by hand, in validate_book(), which is the S3 convention.
Comparing the five systems
The table summarises what the scripts showed, plus the facts that the system pages establish. "Where" says what a user has to install: S3, S4 and Reference Classes come with every R; R6 and S7 are CRAN packages with a version of their own.
| S3 | S4 | Reference Classes | R6 | S7 | |
|---|---|---|---|---|---|
Where it lives |
base R: the |
the |
the |
CRAN package R6 2.6.1 |
CRAN package S7 0.2.2 |
Style |
generic functions; informal |
generic functions; formal classes |
methods belong to the class; formal fields |
methods belong to the class; |
generic functions; formal classes |
Dispatch |
single, on the first argument; implicit classes |
multiple, on any arguments in the signature |
single: |
single: |
multiple, on the arguments chosen for the generic |
Class definition |
none: set the class attribute; constructor and validator by convention |
|
|
|
|
Mutability |
copy-on-modify |
copy-on-modify |
reference semantics (an environment); |
reference semantics (an environment); |
copy-on-modify |
Field checks |
none unless you write them |
slot types checked on assignment; |
field classes checked on assignment |
none unless you write them |
property types checked on assignment; |
Inheritance |
class vector; |
|
|
|
|
|
what the object is built from, usually |
|
|
|
|
Typical use |
base R and most CRAN packages: |
Bioconductor and |
older code that needs mutable objects |
stateful objects: Shiny’s session objects (R6), caches, handles |
new packages; ggplot2 4.0 builds its plot objects with it |
The "typical use" row is based on what the example below prints, not on popularity figures. Two of the cells deserve a comment. In the S4 cell (Bioconductor and Matrix), the classes of Matrix, which ships with R, are S4 classes, as the output below shows, and Bioconductor’s core packages are written with S4 too; this page checked Matrix and not Bioconductor. In the S7 cell (ggplot2 4.0), the ggplot2 NEWS records that the S3 parts of ggplot2 have been replaced with S7 pieces, and a plot object is now an S7_object.
# Which object systems do packages in the wild use?
library(Matrix)
sparse <- Matrix(c(0, 1, 0, 2), 2L, 2L, sparse = TRUE)
class(sparse)
#> [1] "dtCMatrix"
#> attr(,"package")
#> [1] "Matrix"
isS4(sparse)
#> [1] TRUE
library(ggplot2)
p <- ggplot()
class(p)
#> [1] "ggplot2::ggplot" "ggplot" "ggplot2::gg" "S7_object"
#> [5] "gg"
isS4(p)
#> [1] FALSE
typeof(p)
#> [1] "object"
S7::S7_inherits(p)
#> [1] TRUE
packageVersion("ggplot2")
#> [1] ‘4.0.3’
A sparse Matrix object has an S4 class with the attribute "package", and isS4() says so. A ggplot2 4.0.3 plot is an S7 object (type "object", S7_inherits() true), and its class vector still has S3 names so that old S3 code keeps working. In the next block, a Shiny mock session is an R6 object, an environment, while a fitted lm is the classical S3 case, a list with a class, and a connection is an S3-classed integer that refers to a C-level structure.
library(shiny)
session <- MockShinySession$new()
class(session)
#> [1] "MockShinySession" "R6"
typeof(session)
#> [1] "environment"
# Base R itself: lm fits are S3 lists, connections are S3 integers.
fit <- lm(dist ~ speed, data = cars)
c(class(fit), typeof(fit))
#> [1] "lm" "list"
con <- textConnection("a")
c(class(con), typeof(con))
#> [1] "textConnection" "connection" "integer"
close(con)
Choosing a system
There is no single best system; there is a best system for each kind of problem. The questions below are the ones that settle most cases.
-
Do you only need a print, format or summary method for a result? Use S3. It is what the language itself uses and what the R Language Definition singles out as the greatest use of object orientation in R: print, summary and plot methods. A class vector, a constructor and one
UseMethod()generic are enough, and the S3 Classes and Generics page shows the constructor, validator and helper pattern with thebookclass. -
Are you extending a base-R or CRAN class, or writing a package that other packages extend? Use S3 unless the other side is S4. S3 methods interoperate with everything,
inherits()andUseMethod()work across packages, and the mechanism needs no dependency. -
Do you need formal, checked structure, or dispatch on more than one argument? Use S4 if the
methodspackage is acceptable, or S7 if a CRAN dependency is. S4 is the established choice in Bioconductor andMatrix; S7 is newer, simpler to read, and designed to interoperate with S3 and S4. -
Do you need an object that changes in place, with identity? Use R6 (or a Reference Class). A connection-like handle, a cache, a queue and an application component all have an identity that outlives a function call. Reference semantics also cost something: the object is shared by every name that points to it, and R functions that expect value semantics (modify-in-copy) do not apply.
-
Are you starting a new package and want a modern formal system? S7 is the system the R Consortium Object-Oriented Programming working group designed for that, with R Core members taking part; its documentation describes it as somewhat experimental, and its stated long-term goal is to merge it into base R. As of this page it is a CRAN package at version 0.2.2 and is not part of base R 4.6.1. The The S7 Object System page covers its status and API.
Mixing is normal. A package may use S3 for results, S4 for a few core classes and R6 for a stateful component; setOldClass() and S7’s compatibility helpers exist because of that. What does not work well is mixing semantics in one class: an S3 list with reference semantics bolted on, for example, or an R6 object that is expected to be copied by <-.
OBJSXP and the road to S7
The R 4.4.0 NEWS contains an entry for C developers that matters for everyone who follows R’s object systems: the SEXP type S4SXP was renamed OBJSXP, "to support experimenting with alternative object systems". The old name can still be used in C code but is deprecated, and the change is based on contributions from the R Consortium’s Object-Oriented Programming Working Group (R NEWS: Changes in R 4.4.0). The reason is in R Internals: an S4 object is either a simple type with S4 class information or an object of type OBJSXP, and OBJSXP is for S4 and other objects which do not consist solely of a simple type (R Internals: SEXPTYPEs). The old name tied the type to S4; the new name says what the type is, a general type for objects that are not a simple vector or function, so that a package may create such objects without the S4 bit.
library(S7)
book_s4 <- setClass("BookS4", slots = c(title = "character"))
s4_book <- book_s4(title = "Ten Winters")
book_s7 <- new_class("BookS7", properties = list(title = class_character))
s7_book <- book_s7(title = "Ten Winters")
# Both are objects that are not a simple vector or function: type OBJSXP.
# typeof() reports "S4" when the S4 bit is set and "object" when it is not.
typeof(s4_book)
#> [1] "S4"
isS4(s4_book)
#> [1] TRUE
typeof(s7_book)
#> [1] "object"
isS4(s7_book)
#> [1] FALSE
Source: R Internals: S4 objects
On R 4.6.1, typeof() returns "S4" for an OBJSXP object that has the S4 bit set, and "object" for one that does not. The S7 object is the second case: it is a plain OBJSXP object with a class attribute, not an S4 object (isS4() is FALSE). The Language Definition’s table of typeof() values still lists "S4" and not "object", while the typeof() help page lists both.
# An S4 class that extends a basic type keeps that type.
measure <- setClass("Measure", contains = "numeric")
lengths_cm <- measure(c(1.5, 2.5))
typeof(lengths_cm)
#> [1] "double"
isS4(lengths_cm)
#> [1] TRUE
is.object(lengths_cm)
#> [1] TRUE
lengths_cm@.Data
#> [1] 1.5 2.5
# What an S7 object stores: a class attribute, the class object and the
# property values.
names(attributes(s7_book))
#> [1] "class" "S7_class" "title"
class(s7_book)
#> [1] "BookS7" "S7_object"
packageVersion("S7")
#> [1] ‘0.2.2’
Source: R documentation: typeof
An S4 class that extends a basic type keeps that type: Measure extends numeric, so its instances have the type "double", with the S4 bit set (isS4() is TRUE), and the numbers are available as @.Data. An S7 object stores its class vector, a reference to the class object (S7_class) and one attribute per property. The OBJSXP type itself is the groundwork; the rest of the support for alternative object systems came in 4.3.0, in the hooks that R Core added at the request of the same working group: inherits() accepts a class object through nameOfClass(), the @ operator became an S3 generic, and chooseOpsMethod() resolves a conflict between two Ops methods (R NEWS: Changes in R 4.3.0). The R-devel NEWS, which describes the upcoming R 4.7.0, lists a further low-level function, .OBJSXP(), that enables deparsing of non-S4 objects of type "object" (R-devel NEWS); it does not exist in R 4.6.1 and is not used here.
"R5", the methods package and Rscript
"R5" is not a separate system
Reference Classes are sometimes called "R5". The name is a community nickname, a joke on the sequence S3, S4, R5, and it appears in no R documentation. The help page for setRefClass() calls them reference classes; the facility was added in R 2.12.0 as an experimental part of the methods package, and as implemented they are S4 classes whose instances hold an environment, which is why isS4() is TRUE for them above. If a tutorial says "R5", read "Reference Classes" and look for setRefClass(); the Reference Classes and R6 page uses the documented name.
The methods package is attached by Rscript
The S4 and Reference Class machinery lives in the methods package. In an interactive session or with R -f it is attached by default, but before R 3.5.0 Rscript was different: it started with a shorter list of default packages that did not include methods, so scripts that called setClass() or new() needed library(methods). R 3.5.0 changed that. The NEWS entry says that, if --default-packages is not used, Rscript now checks the environment variable R_SCRIPT_DEFAULT_PACKAGES, which takes precedence over R_DEFAULT_PACKAGES, and that if default packages are not specified by either, Rscript now uses the same default packages as R. The previous behaviour, without methods, can be restored by setting the environment variable R_SCRIPT_LEGACY to yes (R NEWS: Changes in R 3.5.0, NEWS.3).
"package:methods" %in% search()
#> [1] TRUE
getOption("defaultPackages")
#> [1] "datasets" "utils" "grDevices" "graphics" "stats" "methods"
exists("setClass")
#> [1] TRUE
environmentName(environment(setClass))
#> [1] "methods"
Sys.getenv("R_SCRIPT_LEGACY", unset = "unset")
#> [1] "unset"
# A child Rscript started with a smaller list of default packages.
rscript <- file.path(R.home("bin"), "Rscript")
check <- shQuote("\"package:methods\" %in% search()")
system2(rscript, c("--default-packages=utils", "-e", check), stdout = TRUE)
#> [1] "[1] FALSE"
Source: R documentation: Rscript
On R 4.6.1 methods is on the search path, defaultPackages lists it, and setClass lives in its namespace. The last call starts a child Rscript with --default-packages=utils, and that process reports FALSE: the methods package is not attached unless the list asks for it. A script that is run with a restricted package list, for example under a container entry point that sets R_DEFAULT_PACKAGES (the Startup help page gives utils,grDevices,graphics,stats as the setting used by R CMD check), can therefore fail with "could not find function "setClass"", and the fix is library(methods) at the top, or methods::setClass(). In a package, importing from methods in NAMESPACE is the correct fix. Examples on the S4 and Reference Class pages do not need library(methods) under Rscript 3.5.0 or later, but it does no harm.
Bridging S3 to S4: setOldClass()
S4 slots, S4 method signatures and S4 generics know the classes that setClass() defined and the basic classes; they do not know an S3 class such as "book". setOldClass("book") registers the S3 class as a virtual S4 class, which lets it appear in a slot type or a method signature. The argument is the character vector that would be the class attribute of the S3 object, so a vector of several names registers the whole inheritance chain; the help page also describes how to attach the known attributes of an S3 class through S4Class.
source("scenario.R")
# An S3 class that S4 has not heard of cannot be used as a slot type.
result <- tryCatch(
setClass("LoanTicket", slots = c(item = "book")),
warning = conditionMessage
)
result
#> [1] "undefined slot classes in definition of \"LoanTicket\": item(class \"book\")"
# setOldClass() registers the S3 class as a virtual S4 class.
setOldClass("book")
isVirtualClass("book")
#> [1] TRUE
loan_ticket <- setClass("LoanTicket", slots = c(item = "book"))
ticket <- loan_ticket(item = book("Ten Winters"))
class(ticket@item)
#> [1] "book"
is(ticket@item, "book")
#> [1] TRUE
# S4 generics can now dispatch on the S3 class.
setGeneric("title_of", function(x) standardGeneric("title_of"))
#> [1] "title_of"
setMethod("title_of", "book", function(x) x$title)
title_of(ticket@item)
#> [1] "Ten Winters"
Source: R documentation: setOldClass
Without the registration, setClass() signals the warning "undefined slot classes in definition of …" and the slot is unusable for checking. After setOldClass("book") the class is virtual, an S4 slot can hold a book, and an S4 generic can dispatch on it. S7 has its own helper for the same purpose, new_S3_class(), covered on its page. The class help page adds the converse: S3 method selection treats objects of an S4 class as if they had the appropriate S3 class attribute, so S3 methods can be defined for S4 classes, and inherits() behaves consistently with is() apart from conditional superclasses.
Pitfalls collected
-
class(x) == "name"instead ofinherits(x, "name"). Since R 4.0.0 a matrix has two classes, so the comparison has length 2, and in anifcondition of length greater than one R 4.2.0 and later signal an error. The NEWS entry for R 4.0.0 says explicitly that the change invalidates code that assumesclass(matrix_obj)has length one, and the R 4.2.0 entry forks.test(), whose result now has classc("ks.test", "htest"), warns packages to useinherits()and not==on a class. -
mode()as the type.mode(1L)is"numeric",mode(sum)is"function",mode(quote(x))is"name". Usetypeof()for the storage type andinherits()oris()for class questions. -
inherits(x, "numeric")for an integer vector. It isFALSEfor a plain integer, sinceclass(1L)is"integer";is.numeric(1L)isTRUE. The implicit class used for dispatch (.class2()) contains"numeric", butinherits()does not use it. -
is.object()on atomic vectors. A plain vector, a matrix and a function are not objects; the test is the class attribute, not the object-orientation of the type. -
Assigning a class to a value of the wrong shape.
class(y) <- "matrix"needs a dimension attribute of length 2, andclass()<-onNULLis an error. -
Missing S3 methods. Many S3 methods are hidden in namespaces. R FAQ 7.26 ("Where have all the methods gone?") says this has the advantage that they cannot be called inadvertently with arguments of the wrong class, and shows
getS3method("[", "terms")to see one andpkg:::foofor an unexported function. Do not call unexported functions in your own code. -
Looking for the source of a primitive.
sum,+andifhave no R source (R FAQ 7.40 and the section on primitives above).
m <- matrix(1:4, 2)
# class(x) is a character vector, so == compares element by element.
class(m)
#> [1] "matrix" "array"
class(m) == "matrix"
#> [1] TRUE FALSE
tryCatch(
if (class(m) == "matrix") "a matrix" else "something else",
error = conditionMessage
)
#> [1] "the condition has length > 1"
# inherits() asks the question that was meant.
inherits(m, "matrix")
#> [1] TRUE
is.matrix(m)
#> [1] TRUE
# mode() is the S-compatible grouping of types, not the type itself.
x <- c(1L, 2L)
c(typeof = typeof(x), mode = mode(x), storage.mode = storage.mode(x))
#> typeof mode storage.mode
#> "integer" "numeric" "integer"
c(typeof = typeof(sum), mode = mode(sum), class = class(sum))
#> typeof mode class
#> "builtin" "function" "function"
mode(x) <- "character"
x
#> [1] "1" "2"
storage.mode(m) <- "double"
typeof(m)
#> [1] "double"
The script shows the first two pitfalls with their real outputs. class(m) == "matrix" is TRUE FALSE, and using it as a condition fails with the message . inherits(m, "matrix") and is.matrix(m) ask the intended question. The third block shows the two replacement forms from earlier: mode(x) <- "character" converted the values, while storage.mode(m) <- "double" changed the matrix from integer to double and kept its dimensions.
Version notes
| Version | Change |
|---|---|
R 1.4.0 (2001) |
The |
R 2.12.0 (2010) |
Reference-based classes are added experimentally to the |
R 3.5.0 |
|
R 4.0.0 |
Matrices also inherit from class |
R 4.3.0 |
|
R 4.4.0 |
The SEXP type |
R-devel (4.7.0, upcoming) |
|
R6 2.6.1, S7 0.2.2 |
Package versions this page checked. S7 0.1.0 was published on 2023-08-24; S7 is a CRAN package, not part of base R 4.6.1. |
The R 3.5.0 entry is in the 3.x NEWS (NEWS.3) and the 1.4.0 and 2.12.0 entries are in the archived NEWS files cited in the history section.
Where older tutorials differ
| Older material says | Current practice | Why |
|---|---|---|
“ |
Not needed since R 3.5.0; still harmless |
|
"Reference Classes are R5", "S4 and R5 are the future" |
Call them Reference Classes; choose the system by the problem |
"R5" is a community nickname, and no R documentation uses it. S4 and Reference Classes are established and maintained, and S7 is the new design for new packages, but none of the systems is deprecated. |
"S4 is the modern OOP system" |
S4 for established ecosystems such as Bioconductor and |
S7 is designed for the same job with a simpler syntax and interoperability with S3 and S4. |
|
|
|
|
|
|
|
|
The first form fails with . |
“A matrix has class |
|
NEWS R 4.0.0. |
|
|
|
|
|
The type was renamed in R 4.4.0; the old name still compiles but is deprecated. |
Treating |
Call the R-level closure; use |
|
Source: R Internals: SEXPTYPEs
For the full list of changes by release see What’s New and Migrating Older Code.
Where to go next
-
S3 Classes and Generics for the class attribute, constructors, validators,
UseMethod(),NextMethod()and method registration. -
S3 Group Generics and Operator Overloading for the
Math,Ops,SummaryandComplexgroups andchooseOpsMethod(). -
S4 Classes and Methods for slots, validity, generics, multiple dispatch and class unions.
-
Reference Classes and R6 for mutable objects with reference semantics.
-
The S7 Object System for the newest system and a field-by-field comparison on the
bookclass. -
Attributes and Names for the
classattribute as one attribute among others. -
Functions for closures, primitives and
do.call(), and Memory and Copy Semantics for when R copies and when it does not. -
Language Objects and Quoting for calls and symbols, whose types and classes appeared in the first table.
References
-
The R Language Definition: Objects (Basic types, Builtin objects and special forms), Object-oriented programming (Definition)
-
R Internals: SEXPTYPEs, Argument evaluation, S4 objects, .Internal vs .Primitive (Special primitives, Prototypes for primitives)
-
Writing R Extensions: Writing portable packages
-
R FAQ 3.1: What is S?
-
R FAQ 7.26: Where have all the methods gone?
-
R FAQ 7.40: How do I access the source code for a function?
-
R NEWS: Changes in R 4.0.0, 4.3.0 and 4.4.0; NEWS.3: R 3.5.0; NEWS.2: R 2.12.0; NEWS.1: R 1.4.0; R-devel NEWS