The S7 Object System
|
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. This page uses the add-on package S7 version 0.2.2 (the CRAN release of 2026-04-22), ggplot2 version 4.0.3 for one status check, and R6 version 2.6.1 for the comparison with the other systems; the S4 examples use the methods package that ships with R, and the lintr 3.4.0 default linters were used only to check the scripts. Only what S7 0.2.2 does is described. S7 is a package for object-oriented programming that two older systems of the language, S3 and S4, did not quite give R: formal classes with typed properties, validation on every change, ordinary generic functions with explicit method lookup and explicit calls to the parent method, and compatibility with both older systems. Its authors present it as a successor to S3 and S4, and its long-term goal is to be merged into base R, but it is an ordinary CRAN package today and its own site calls it somewhat experimental.
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. S7 classes are written in CamelCase, which the default object_name_linter flags, so each script is wrapped in a # nolint start: object_name_linter. and # nolint end pair of comments (a listing may leave out the first lines of its script when they only attach S7, source the scenario, open the lint exclusion and define show_error(), a helper that returns the message of an error, as the error listing of the book class below does; the script that checks warnings defines a show_warning() in the same way, and each script ends with the # nolint end comment). The names books, loans, shelf_today, loan_days 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"), which also provides is_valid_isbn13() and normalise_isbn(). The pkgdown site of S7 is built from the development sources, so it also describes features that the CRAN release 0.2.2 does not have (the := operator that names a class, for example, is demonstrated below to fail on 0.2.2); the links to the site below are to pages that describe the behaviour shown, and every claim about behaviour was run on 0.2.2. How the four systems that this page compares relate to each other is the subject of OOP Overview and Base Types; the older systems have pages of their own: S3 Classes and Generics, S4 Classes and Methods and Reference Classes and R6.
What S7 is and where it stands
S7 was designed and implemented by the R Consortium Object-Oriented Programming Working Group, a group that includes members of R Core, of Bioconductor, of Posit and the tidyverse, and of the wider R community (S7 package documentation: S7). Its motivation is a list of difficulties that the group found in practice with the two systems that ship with R (S7 package documentation: Motivation for S7):
-
S3 has no formal class definition, so nothing says which properties an object has, and its attributes are read with
attr(), which matches partially and returnsNULLfor a misspelt name. S7 describes a class with an object made bynew_class()and reads properties with@, which does neither. -
The dispatch rules of S3 are complicated and
NextMethod()depends on the whole class hierarchy and on what methods happen to be registered. S7 asks for an explicitsuper()call that names the parent class. -
S3 is built for single dispatch. S7 provides multiple dispatch for any generic.
-
S4 allows multiple inheritance and picks methods with a distance metric that is hard to predict. S7 has single inheritance and a simpler lookup. S4 was also a clean break from S3, while S7 objects are S3 objects, which is what makes it possible to convert an existing package with little work.
-
Properties can be dynamic (computed on demand), so the representation of an object can change while code that reads
x@namekeeps working.
The standing of the package needs a careful statement, because it is easy to find it overstated. As of R 4.6.1 S7 is not part of base R: it has to be installed from CRAN and attached or called with S7::, and exists("new_class", envir = baseenv()) is FALSE. The site of the package says that its long-term goal is to merge S7 into base R, that the design is considered sound, but that the package is somewhat experimental because it has little use in the wild so far and breaking changes are possible (S7 package documentation: S7). The one thing that did arrive in base R is groundwork. R 4.4.0 renamed the internal type S4SXP to OBJSXP "to support experimenting with alternative object systems", with contributions from the same working group (R NEWS: Changes in R 4.4.0; the type table is in R Internals: SEXPTYPEs). An S7 object is such an object, and typeof() prints "object" for it, as the first script shows. R 4.3.0 had already made the @ operator an S3 generic (R NEWS: Changes in R 4.3.0), which is what lets @ read the properties of an S7 object. The base-types background is in OOP Overview and Base Types.
The package is nevertheless used by a very widely installed one. The ggplot2 4.0.0 NEWS lists, under breaking changes, that "the S3 parts of ggplot2 have been replaced with S7 bits" (ggplot2 NEWS), and the script below shows what a plot object looks like in ggplot2 4.0.3. The practical consequence for a reader of this page is the same as for any package that exports S7 classes: the plot objects are S7 objects, @ and prop_names() work on them (p@data is the data frame of the plot), and their class vector now includes "S7_object".
library(S7)
source("scenario.R")
# nolint start: object_name_linter.
# S7 is an add-on package, not part of base R.
packageVersion("S7")
#> [1] ‘0.2.2’
exists("new_class", envir = baseenv())
#> [1] FALSE
environmentName(environment(new_class))
#> [1] "S7"
# An S7 object is a base-R "object" (type OBJSXP since R 4.4.0) with a class
# attribute that has two elements: the class itself and S7_object.
Book <- new_class("Book", properties = list(title = class_character))
b <- Book(title = "Dune")
typeof(b)
#> [1] "object"
class(b)
#> [1] "Book" "S7_object"
is.object(b)
#> [1] TRUE
isS4(b)
#> [1] FALSE
names(attributes(b))
#> [1] "class" "S7_class" "title"
# ggplot2 4.0 builds its plot objects with S7: the data of a plot is a property.
library(ggplot2)
packageVersion("ggplot2")
#> [1] ‘4.0.3’
p <- ggplot(books, aes(year, pages))
class(p)
#> [1] "ggplot2::ggplot" "ggplot" "ggplot2::gg" "S7_object"
#> [5] "gg"
S7_inherits(p)
#> [1] TRUE
typeof(p)
#> [1] "object"
class(p@data)
#> [1] "data.frame"
prop_names(p)
#> [1] "data" "layers" "scales" "guides" "mapping"
#> [6] "theme" "coordinates" "facet" "layout" "labels"
#> [11] "meta" "plot_env"
# The := operator that some pages of the S7 site use to name a class is not
# part of S7 0.2.2: name the class with new_class("Dog", ...) as in the rest
# of this page.
code <- "Dog := new_class(properties = list(name = class_character))"
tryCatch(eval(parse(text = code)), error = \(e) conditionMessage(e))
#> [1] "could not find function \":=\""
# nolint end
Five things to take from the output. The attached package is version 0.2.2 and new_class is not in the base environment. An S7 object has type "object" (the OBJSXP type of R 4.4.0 and later), it is an object for is.object() and not an S4 object for isS4(), and its attributes are class, S7_class and one attribute per property: S7 stores the class definition inside every instance, and properties are attributes. The S3 class vector has two elements, the class name and S7_object. ggplot2 4.0.3 builds its plots as S7 objects, which S7_inherits() confirms with one argument. And the := operator used by the development documentation does not exist in 0.2.2: code copied from the site fails with could not find function ":=", and the working form is Dog <- new_class("Dog", ...).
Classes with new_class()
A class is defined by calling new_class() and is represented by the object it returns. That object is also the constructor: calling it makes instances. The result should be assigned to a variable with the same name as the first argument, the class name, which is what appears in messages and in the S3 class vector. The arguments are:
| Argument | Meaning |
|---|---|
|
The class name, a string. By convention CamelCase and equal to the variable the result is assigned to. |
|
The parent class: another S7 class, a base class such as |
|
The package that defines the class, or |
|
A named list of properties. A plain class ( |
|
|
|
A function that replaces the default constructor. It must end with a call to |
|
A function of one argument, |
The book of the shelf scenario becomes the class below, with the six fields of book() in the same order and the same rules as validate_book() in scenario.R: every field has length one, the title is not empty, a present ISBN passes is_valid_isbn13(), and the year is at most 2100. The default = NA_character_ of each property matters: without a default a property starts as a zero-length vector (character(0)), and a validator that insists on length one would reject the class’s own default object, as the last part of the second script shows.
library(S7)
source("scenario.R")
# nolint start: object_name_linter.
# The six scenario fields, each a property with a type and an NA default.
Book <- new_class("Book",
properties = list(
title = new_property(class_character, default = NA_character_),
author = new_property(class_character, default = NA_character_),
isbn = new_property(class_character, default = NA_character_),
year = new_property(class_integer, default = NA_integer_),
genre = new_property(class_character, default = NA_character_),
pages = new_property(class_integer, default = NA_integer_)
),
# The same rules as validate_book() in scenario.R. The validator returns NULL
# (valid) or a character vector of problems.
validator = function(self) {
if (any(lengths(props(self)) != 1L)) {
return("every property of a book must have length 1")
}
if (is.na(self@title) || !nzchar(self@title)) {
return("a book needs a title")
}
if (!is.na(self@isbn) && !is_valid_isbn13(self@isbn)) {
return(paste("invalid ISBN-13:", self@isbn))
}
if (!is.na(self@year) && self@year > 2100L) {
return(paste("implausible year:", self@year))
}
NULL
}
)
dune <- Book(
title = "Dune", author = "Frank Herbert", isbn = "9780306406157",
year = 1965L, genre = "Science fiction", pages = 412L
)
dune
#> <Book>
#> @ title : chr "Dune"
#> @ author: chr "Frank Herbert"
#> @ isbn : chr "9780306406157"
#> @ year : int 1965
#> @ genre : chr "Science fiction"
#> @ pages : int 412
class(dune)
#> [1] "Book" "S7_object"
dune@title
#> [1] "Dune"
dune@year
#> [1] 1965
Printing an instance shows the class name and one line per property, with the type and the value. Instances are created like function calls with named arguments, and there is no new(): the class object is the constructor. Printing the class itself shows its parent, the arguments of its constructor, whether it has a validator, and its properties with their types:
# The class object describes itself.
Book
#> <Book> class
#> @ parent : <S7_object>
#> @ constructor: function(title, author, isbn, year, genre, pages) {...}
#> @ validator : function(self) {...}
#> @ properties :
#> $ title : <character>
#> $ author: <character>
#> $ isbn : <character>
#> $ year : <integer>
#> $ genre : <character>
#> $ pages : <integer>
The failures all come from S7 and their wording is stable enough to recognise. A value that has the wrong type is stopped before the validator runs, and the message starts <Book> object properties are invalid: and lists each property with a leading @; a validator failure starts <Book> object is invalid: and lists the strings the validator returned, each with a leading dash; a misspelt argument is the ordinary R error unused argument, and a misspelt property is Can't find property <Book>@subtitle, because @ does not match partially. The example year = 1965 is the common trap: 1965 is a double, so the type check rejects it, and the integer literal 1965L (or a constructor that coerces, see below) is required.
# Every failure is an ordinary error; show_error() returns its message.
show_error <- function(expr) {
tryCatch({
expr
"no error"
}, error = \(e) conditionMessage(e))
}
cat(show_error(Book(title = "")), sep = "\n")
#> <Book> object is invalid:
#> - a book needs a title
cat(show_error(Book(title = "Dune", isbn = "9780306406158")), sep = "\n")
#> <Book> object is invalid:
#> - invalid ISBN-13: 9780306406158
cat(show_error(Book(title = "Dune", year = 2200L)), sep = "\n")
#> <Book> object is invalid:
#> - implausible year: 2200
cat(show_error(Book(title = c("Dune", "Emma"))), sep = "\n")
#> <Book> object is invalid:
#> - every property of a book must have length 1
cat(show_error(Book(title = "Dune", year = 1965)), sep = "\n")
#> <Book> object properties are invalid:
#> - @year must be <integer>, not <double>
cat(show_error(Book(title = "Dune", cover = "red")), sep = "\n")
#> unused argument (cover = "red")
cat(show_error(dune@subtitle), sep = "\n")
#> Can't find property <Book>@subtitle
# Types are class objects, not strings: "character" is not a class here.
cat(show_error(new_class("Z", properties = list(a = "character"))), sep = "\n")
#> Can't convert `property$a` to a valid class. Class specification must be an S7 class object, the result of `new_S3_class()`, an S4 class object, or a base class, not a <character>.
# Without default =, a property starts empty (length 0). A validator that wants
# exactly one value then rejects the class's own default object.
Strict <- new_class("Strict",
properties = list(title = class_character),
validator = function(self) {
if (length(self@title) != 1L) {
"@title must have length 1"
}
}
)
cat(show_error(Strict()), sep = "\n")
#> <Strict> object is invalid:
#> - @title must have length 1
Strict(title = "Dune")
#> <Strict>
#> @ title: chr "Dune"
# nolint end
Two more messages are in that listing. A type given as a string, as in list(a = "character"), is refused with an error that begins Can't convert and says that the class specification must be an S7 class object, the result of new_S3_class(), an S4 class object or a base class, because a property type is a class object (class_character), an S7 class, an S3 class declared with new_S3_class() or an S4 class. And the Strict class shows the zero-length default at work: its own default object is invalid, so Strict() fails and Strict(title = "Dune") works.
Properties with new_property()
A property is the S7 counterpart of an S3 attribute or an S4 slot. In its simplest form it is just a class, which S7 checks on every assignment. new_property() adds four things that a plain class cannot express:
-
default, the value a new object gets when the constructor is not given one. A quoted call, such asquote(Sys.time()), becomes a lazy default evaluated at construction. -
validator, a function of the value that returnsNULLor a message. The message should not repeat the property name: S7 prepends@name, which lets one property definition be reused for several properties. -
setter, a function ofselfandvaluethat returns the modified object. It runs on every assignment, including the one made by the constructor, and is the place to normalise a value. -
getter, a function ofselfthat computes the value on demand. A property with a getter and no setter is read-only. A property with both is fully dynamic and can be assigned.
The class below adds three of them to the book: an isbn setter that normalises the ISBN with normalise_isbn() from the scenario, a validator for pages and an age property computed from year and shelf_today. The assignment self@isbn <- value inside the setter does not call the setter again.
# new_property() adds what a plain class cannot say: a default, a validator for
# one property, a setter and a getter.
Book <- new_class("Book",
properties = list(
title = new_property(class_character, default = NA_character_),
author = new_property(class_character, default = NA_character_),
# A setter returns the modified object; this one normalises the ISBN.
isbn = new_property(
class_character,
default = NA_character_,
setter = function(self, value) {
if (length(value) == 1L && !is.na(value)) {
value <- normalise_isbn(value)
}
self@isbn <- value
self
}
),
year = new_property(class_integer, default = NA_integer_),
genre = new_property(class_character, default = NA_character_),
# A property validator returns a message; S7 adds "@pages" itself.
pages = new_property(
class_integer,
default = NA_integer_,
validator = function(value) {
if (length(value) == 1L && !is.na(value) && value < 1L) {
"must be positive"
}
}
),
# A getter makes the property computed: read-only without a setter.
age = new_property(
class_integer,
getter = function(self) {
if (is.na(self@year)) {
return(NA_integer_)
}
as.integer(format(shelf_today, "%Y")) - self@year
}
)
),
validator = function(self) {
if (is.na(self@title) || !nzchar(self@title)) {
"a book needs a title"
}
}
)
dune <- Book(title = "Dune", isbn = "978-0-306-40615-7", year = 1965L)
dune
#> <Book>
#> @ title : chr "Dune"
#> @ author: chr NA
#> @ isbn : chr "9780306406157"
#> @ year : int 1965
#> @ genre : chr NA
#> @ pages : int NA
#> @ age : int 61
dune@isbn
#> [1] "9780306406157"
dune@age
#> [1] 61
The computed property is printed with the others, but it is not an argument of the constructor and it cannot be assigned. The setter normalised the hyphenated ISBN, in the constructor as well as on later assignments, and the property validator rejects a non-positive page count with the message <Book>@pages must be positive when the assignment is made, and with the longer properties are invalid message when the constructor is called.
# The computed property is not a constructor argument and cannot be assigned.
names(formals(Book))
#> [1] "title" "author" "isbn" "year" "genre" "pages"
cat(show_error(Book(title = "Dune", age = 3L)), sep = "\n")
#> unused argument (age = 3)
cat(show_error(dune@age <- 3L), sep = "\n")
#> Can't set read-only property <Book>@age
# The setter runs on every assignment; the property validator too.
dune@isbn <- "978 0 306 40615 7"
dune@isbn
#> [1] "9780306406157"
cat(show_error(dune@pages <- 0L), sep = "\n")
#> <Book>@pages must be positive
cat(show_error(Book(title = "Dune", pages = 0L)), sep = "\n")
#> <Book> object properties are invalid:
#> - @pages must be positive
Properties are read and written with @, and the same operations exist as functions for code that holds the property name in a variable: prop(x, name), prop(x, name) <- value, prop_names(), prop_exists(), props() (all properties as a named list), props(x) <- list(...) and set_props() (which returns a modified copy). Assigning several properties with props<-() or set_props() validates once, after all of them have changed, which matters when an intermediate state would be invalid; the next section uses that. S7 objects have value semantics like every ordinary R object: set_props() returns a copy and the original is unchanged. The @ operator on an S7 object requires R 4.3.0 or later (it became an S3 generic then, R NEWS: Changes in R 4.3.0); the documentation of the @ operator is R documentation: slotOp.
# Reading and writing by name: prop(), props(), prop_names(), prop_exists().
prop_names(dune)
#> [1] "title" "author" "isbn" "year" "genre" "pages" "age"
prop(dune, "title")
#> [1] "Dune"
prop(dune, "title") <- "Dune Messiah"
dune@title
#> [1] "Dune Messiah"
prop_exists(dune, "age")
#> [1] TRUE
prop_exists(dune, "narrator")
#> [1] FALSE
# props<-() and set_props() change several properties and validate once.
props(dune) <- list(title = "Children of Dune", year = 1976L)
c(dune@title, dune@age)
#> [1] "Children of Dune" "50"
dune2 <- set_props(dune, title = "God Emperor of Dune")
c(dune@title, dune2@title)
#> [1] "Children of Dune" "God Emperor of Dune"
# A property is itself an object that can be inspected.
Book@properties$age
#> <S7_property>
#> $ name : chr "age"
#> $ class : <S7_base_class>: <integer>
#> $ getter : function (self)
#> $ setter : NULL
#> $ validator: NULL
#> $ default : NULL
# nolint end
Source: S7 package documentation: props
The last line shows that a property is an object too: Book@properties$age prints its name, class, getter, setter, validator and default, and the class object itself is a list-like value whose parts (@name, @parent, @properties, @validator, @constructor, @abstract, @package) can be read.
Validation: when it runs and how to avoid it
S7 validates an object at three moments: when the constructor builds it, whenever a property is assigned with @<- or prop<-, and when validate() is called. Validation has two layers. First S7 checks the declared type of every property and the property validators (properties = TRUE, the default of validate()). If all of them pass, the validator of the class runs, and then the validators of the parent classes up the chain (recursive = TRUE); the problems of all the classes are collected and reported together. The class validator is the place for rules that involve several properties, and property validators are the place for rules about one.
An assignment that fails leaves the object unchanged (the script prints the title after a rejected assignment). Direct use of attr() bypasses the checks, which is what validate() is for. The script below shows both, and then a case where checking after every assignment is a nuisance: a Loan whose return date must not precede its loan date (row 12 of the scenario’s loans is such a loan, and it is rejected by the constructor), and which is moved by changing both dates. Moving them one at a time passes through an invalid state, so the first assignment is rejected. props<-() assigns both at once and validates once, and valid_eventually() runs any function on the object with validation switched off and validates at the end. valid_implicitly() is the same without the final check, for performance-critical code that cannot produce an invalid object.
# A trimmed Book (title and year) keeps the listings short.
Book <- new_class("Book",
properties = list(
title = new_property(class_character, default = NA_character_),
year = new_property(class_integer, default = NA_integer_)
),
validator = function(self) {
if (is.na(self@title) || !nzchar(self@title)) {
"a book needs a title"
}
}
)
dune <- Book(title = "Dune", year = 1965L)
# Assignment validates: a rejected assignment leaves the object unchanged.
cat(show_error(dune@title <- ""), sep = "\n")
#> <Book> object is invalid:
#> - a book needs a title
cat(show_error(dune@year <- "1965"), sep = "\n")
#> <Book>@year must be <integer>, not <character>
dune@title
#> [1] "Dune"
# Low-level attr() bypasses the checks; validate() finds the damage.
broken <- dune
attr(broken, "title") <- ""
cat(show_error(validate(broken)), sep = "\n")
#> <Book> object is invalid:
#> - a book needs a title
# Validation can reject a state that is only temporarily wrong. A loan must
# not be returned before it was borrowed: row 12 of loans is exactly that.
Loan <- new_class("Loan",
properties = list(
loan_id = new_property(class_character, default = NA_character_),
loan_date = new_property(class_Date, default = as.Date(NA)),
return_date = new_property(class_Date, default = as.Date(NA))
),
validator = function(self) {
if (!is.na(self@return_date) && self@return_date < self@loan_date) {
"@return_date is before @loan_date"
}
}
)
cat(show_error(Loan(
loan_id = loans$loan_id[[12]],
loan_date = loans$loan_date[[12]],
return_date = loans$return_date[[12]]
)), sep = "\n")
#> <Loan> object is invalid:
#> - @return_date is before @loan_date
loan <- Loan(
loan_id = loans$loan_id[[1]],
loan_date = loans$loan_date[[1]],
return_date = loans$return_date[[1]]
)
loan@loan_date
#> [1] "2025-01-14"
loan@return_date
#> [1] "2025-02-04"
# Moving both dates one by one passes through an invalid state ...
cat(show_error(loan@loan_date <- loan@loan_date + 60), sep = "\n")
#> <Loan> object is invalid:
#> - @return_date is before @loan_date
# ... so change them together: props<-() validates once, at the end.
props(loan) <- list(
loan_date = loan@loan_date + 60,
return_date = loan@return_date + 60
)
loan@loan_date
#> [1] "2025-03-15"
# valid_eventually() runs a function with validation off, then validates.
loan <- valid_eventually(loan, function(object) {
object@loan_date <- object@loan_date - 60
object@return_date <- object@return_date - 60
object
})
loan@loan_date
#> [1] "2025-01-14"
The last part of the script is the contrast with S4 that the table at the end of this page summarises. In S4, assigning to a slot checks that the value has the slot’s class, but it does not run the validity method: s4@title <- "" is accepted and validObject() has to be called by hand to find the problem (R documentation: validObject documents the function, and S4 Classes and Methods describes the S4 side). In S7 the same assignment is an error. R6 fields and S3 list elements are never checked at assignment. The cost of S7’s rule is the one the Loan example shows, and the tools to deal with it are the three functions above. The R-side advice for checking arguments in ordinary functions is in Defensive Programming and Input Validation.
# The S4 contrast: assignment to a slot checks the class, not the validity.
setClass("BookS4",
slots = c(title = "character"),
validity = function(object) {
if (!nzchar(object@title)) "a book needs a title" else TRUE
}
)
s4 <- new("BookS4", title = "Dune")
s4@title <- ""
s4@title
#> [1] ""
cat(show_error(validObject(s4)), sep = "\n")
#> invalid class “BookS4” object: a book needs a title
# nolint end
Source: R documentation: validObject
Constructors and new_object()
The default constructor of a class has one argument per property, in order, with the property defaults as default values (parent properties first). It is a good constructor when the arguments are the properties. When they are not, because the caller should be able to give raw values that the constructor tidies up, supply constructor =. A constructor is an ordinary function that ends with a call to new_object(): the first argument is an object of the parent class (S7_object() when there is no parent), followed by one named argument per property. new_object() creates the object, runs the property setters and validates it, so a custom constructor does not have to call validate().
The constructor below does for the S7 Book what the book() helper does for the S3 book: it trims the strings, normalises the ISBN and coerces the numbers, so the user can pass the values exactly as they come out of books.csv. Because the checks happen in new_object(), the padded title of row 3 is trimmed first and an invalid ISBN is still rejected.
# The default constructor has one argument per property, with the defaults.
Plain <- new_class("Plain",
properties = list(
title = new_property(class_character, default = NA_character_),
year = new_property(class_integer, default = NA_integer_)
)
)
args(Plain)
#> function (title = NA_character_, year = NA_integer_)
#> NULL
# A custom constructor takes whatever arguments it likes and ends with
# new_object(): the parent object first, then one named argument per property.
# This one repeats what the S3 book() helper does: trim, normalise, coerce.
Book <- new_class("Book",
properties = list(
title = new_property(class_character, default = NA_character_),
author = new_property(class_character, default = NA_character_),
isbn = new_property(class_character, default = NA_character_),
year = new_property(class_integer, default = NA_integer_),
genre = new_property(class_character, default = NA_character_),
pages = new_property(class_integer, default = NA_integer_)
),
validator = function(self) {
if (is.na(self@title) || !nzchar(self@title)) {
"a book needs a title"
} else if (!is.na(self@isbn) && !is_valid_isbn13(self@isbn)) {
paste("invalid ISBN-13:", self@isbn)
}
},
constructor = function(title, author = NA_character_, isbn = NA_character_,
year = NA_integer_, genre = NA_character_,
pages = NA_integer_) {
new_object(
S7_object(),
title = trimws(title),
author = trimws(author),
isbn = if (is.na(isbn)) isbn else normalise_isbn(isbn),
year = as.integer(year),
genre = trimws(genre),
pages = as.integer(pages)
)
}
)
# Row 3 of the shelf, as it is in books.csv: the title is padded with blanks.
row <- books[3, ]
quiet <- Book(row$title, row$author, row$isbn, row$year, row$genre, row$pages)
quiet
#> <Book>
#> @ title : chr "the quiet streets"
#> @ author: chr "Mara Quill"
#> @ isbn : chr "9780001000100"
#> @ year : int 2019
#> @ genre : chr "Fiction"
#> @ pages : int 214
quiet@title
#> [1] "the quiet streets"
# A hyphenated ISBN is normalised before it is stored.
Book("Loud Roofs", isbn = "978-0-00-200029-1")@isbn
#> [1] "9780002000291"
# A numeric year is coerced by the constructor, so the type check passes.
Book("Emma", year = 1815)@year
#> [1] 1815
# The constructor still goes through validation.
cat(show_error(Book(" ")), sep = "\n")
#> <Book> object is invalid:
#> - a book needs a title
cat(show_error(Book("Emma", isbn = "9780306406158")), sep = "\n")
#> <Book> object is invalid:
#> - invalid ISBN-13: 9780306406158
# A subclass of a class with a custom constructor needs no constructor of its
# own: its default one forwards ... to the parent constructor.
AudioBook <- new_class("AudioBook", parent = Book,
properties = list(
narrator = new_property(class_character, default = NA_character_),
minutes = new_property(class_integer, default = NA_integer_)
)
)
args(AudioBook)
#> function (title, author = NA_character_, isbn = NA_character_,
#> year = NA_integer_, genre = NA_character_, pages = NA_integer_,
#> narrator = NA_character_, minutes = NA_integer_)
#> NULL
AudioBook("Dune", "Frank Herbert", narrator = "Scott Brick", minutes = 1260L)
#> <AudioBook>
#> @ title : chr "Dune"
#> @ author : chr "Frank Herbert"
#> @ isbn : chr NA
#> @ year : int NA
#> @ genre : chr NA
#> @ pages : int NA
#> @ narrator: chr "Scott Brick"
#> @ minutes : int 1260
# nolint end
A subclass of a class with a custom constructor does not need one of its own. The default constructor of the subclass takes ..., forwards it to the parent constructor, and adds one named argument for each property the subclass introduces, as the printed args(AudioBook) shows (the first arguments are the parent’s, narrator and minutes are the new ones). That is the design choice behind the rule that a constructor must end with new_object(): the object for the parent class is built by the parent constructor, and the child adds its own properties.
Inheritance, abstract classes and class queries
A class is a subclass of another when parent = names it. S7 has single inheritance only: parent is one class. A subclass has the properties of its parent first and then its own, its constructor accepts both sets, and its validator is checked in addition to the parent’s. The AudioBook below adds narrator and minutes to a trimmed Book. The parent part of the object is built and validated first, which is why the first message reports <Book> and not <AudioBook>; validate() checks the whole object against the validator of the class and of every ancestor and reports all the problems.
Book <- new_class("Book",
properties = list(
title = new_property(class_character, default = NA_character_),
author = new_property(class_character, default = NA_character_),
year = new_property(class_integer, default = NA_integer_)
),
validator = function(self) {
if (is.na(self@title) || !nzchar(self@title)) {
"a book needs a title"
}
}
)
# parent = Book: AudioBook gets the parent's properties first, then its own.
AudioBook <- new_class("AudioBook",
parent = Book,
properties = list(
narrator = new_property(class_character, default = NA_character_),
minutes = new_property(class_integer, default = NA_integer_)
),
validator = function(self) {
if (!is.na(self@minutes) && self@minutes < 1L) {
"@minutes must be positive"
}
}
)
args(AudioBook)
#> function (title = NA_character_, author = NA_character_, year = NA_integer_,
#> narrator = NA_character_, minutes = NA_integer_)
#> NULL
dune <- AudioBook(title = "Dune", narrator = "Scott Brick", minutes = 1260L)
dune
#> <AudioBook>
#> @ title : chr "Dune"
#> @ author : chr NA
#> @ year : int NA
#> @ narrator: chr "Scott Brick"
#> @ minutes : int 1260
class(dune)
#> [1] "AudioBook" "Book" "S7_object"
# The constructor builds the parent part first, so the parent's validator
# speaks first (note "<Book>"); the child's validator checks the whole object.
cat(show_error(AudioBook(title = "", minutes = 10L)), sep = "\n")
#> <Book> object is invalid:
#> - a book needs a title
cat(show_error(AudioBook(title = "Dune", minutes = 0L)), sep = "\n")
#> <AudioBook> object is invalid:
#> - @minutes must be positive
# validate() runs the validator of the class and of every parent, and reports
# all the problems together.
broken <- dune
attr(broken, "title") <- ""
attr(broken, "minutes") <- 0L
cat(show_error(validate(broken)), sep = "\n")
#> <AudioBook> object is invalid:
#> - @minutes must be positive
#> - a book needs a title
The questions one asks about a class have separate functions. S7_inherits(x, class) is TRUE when x is an instance of class or of a subclass, and is FALSE rather than an error when x is not an S7 object at all. With class = NULL it tests whether x is any S7 object, which is how the ggplot2 object was checked above. check_is_S7(x) is the assertion form for package code: it returns invisibly or stops with a message of the form must be an <S7_object>, not a <double>, preceded by the argument. The S3 inherits(x, "Book") also works, because the S3 class vector contains every class in the chain. S7_class(x) returns the class object of an instance, and @parent walks up the hierarchy until S7_object, the root, whose instances carry no properties at all.
# Asking about the class.
S7_inherits(dune, Book)
#> [1] TRUE
S7_inherits(dune, AudioBook)
#> [1] TRUE
S7_inherits(Book(title = "Dune"), AudioBook)
#> [1] FALSE
S7_inherits(1, Book)
#> [1] FALSE
inherits(dune, "Book")
#> [1] TRUE
S7_class(dune)@name
#> [1] "AudioBook"
S7_class(dune)@parent@name
#> [1] "Book"
AudioBook@parent@name
#> [1] "Book"
Book@parent@name
#> [1] "S7_object"
class(S7_object())
#> [1] "S7_object"
cat(show_error(check_is_S7(1)), sep = "\n")
#> `1` must be an <S7_object>, not a <double>
check_is_S7(dune)
An abstract class exists only to be inherited from. abstract = TRUE makes its constructor fail with Can't construct an object from abstract class <Item>, while a subclass of it can be constructed, and the abstract class can still be the dispatch class of a method that its subclasses inherit. This is the S7 counterpart of an S4 virtual class (see S4 Classes and Methods); there is no separate "interface" construct, and there is no multiple inheritance, so shared behaviour is expressed with a generic that has a method for the abstract parent, or with a union (below).
# An abstract class cannot be instantiated, but its subclasses can.
Item <- new_class("Item",
abstract = TRUE,
properties = list(
title = new_property(class_character, default = NA_character_)
)
)
Item@abstract
#> [1] TRUE
cat(show_error(Item()), sep = "\n")
#> Can't construct an object from abstract class <Item>
Periodical <- new_class("Periodical",
parent = Item,
properties = list(issue = new_property(class_integer, default = NA_integer_))
)
Periodical(title = "Shelf News", issue = 12L)
#> <Periodical>
#> @ title: chr "Shelf News"
#> @ issue: int 12
# nolint end
Generics and methods
In S7, as in S3 and S4, a method belongs to a generic function and not to a class: a call looks like any other function call, describe(x), and the generic finds the method that matches the class of its arguments. A generic is created with new_generic(name, dispatch_args): the name of the generic (assigned to a variable with the same name, for the same reason as a class) and the character vector of the arguments that take part in dispatch. The default body is a single call to S7_dispatch(), which finds the method for the classes of the dispatch arguments and calls it with the arguments of the generic call. Printing a generic shows its signature and the methods registered so far, so unlike an S3 generic it is easy to find out what exists.
A method is registered with method(generic, class) <- function, and the rest of the page writes it like that. The first arguments of the method must be the dispatch arguments, without default values. A method may add arguments that the generic does not have, provided the generic has ..., which the generic created by new_generic() does. The package documentation advises that the methods themselves normally do not take ..., so that a misspelt argument is an error and not silently swallowed, and says this is a deliberate difference from S3 (S7 package documentation: Generics and methods).
A child class inherits the method of its parent until it has a method of its own. A child method that wants to reuse the parent’s implementation calls the generic on super(x, to = Book): the object is passed through the generic again, but dispatch treats it as an instance of Book. That is the difference from NextMethod() and callNextMethod(): the parent class is named where the call is written, every argument is passed explicitly, and nothing depends on the class vector at run time (S7 package documentation: super). When there is no method for the class of the argument, the error names the generic and the dispatch class, with MISSING for an omitted argument. class_any registers a fallback for every class, S7 or not.
Book <- new_class("Book",
properties = list(
title = new_property(class_character, default = NA_character_),
author = new_property(class_character, default = NA_character_),
year = new_property(class_integer, default = NA_integer_)
)
)
AudioBook <- new_class("AudioBook",
parent = Book,
properties = list(
narrator = new_property(class_character, default = NA_character_)
)
)
# new_generic(name, dispatch_args): the name is the variable name too.
describe <- new_generic("describe", "x")
describe
#> <S7_generic> describe(x, ...) with 0 methods:
# method(generic, class) <- function registers a method.
method(describe, Book) <- function(x, ...) {
sprintf("%s (%s, %s)", x@title, x@author, x@year)
}
describe
#> <S7_generic> describe(x, ...) with 1 methods:
#> 1: method(describe, Book)
dune <- Book(title = "Dune", author = "Frank Herbert", year = 1965L)
describe(dune)
#> [1] "Dune (Frank Herbert, 1965)"
# AudioBook inherits the Book method until it has one of its own, where
# super() calls the parent's method on the same object.
ab <- AudioBook(
title = "Dune", author = "Frank Herbert", year = 1965L,
narrator = "Scott Brick"
)
describe(ab)
#> [1] "Dune (Frank Herbert, 1965)"
method(describe, AudioBook) <- function(x, ...) {
paste0(describe(super(x, to = Book)), ", read by ", x@narrator)
}
describe(ab)
#> [1] "Dune (Frank Herbert, 1965), read by Scott Brick"
# No method for the class: an error that names the dispatch class.
cat(show_error(describe("Dune")), sep = "\n")
#> Can't find method for `describe(<character>)`.
cat(show_error(describe()), sep = "\n")
#> Can't find method for `describe(MISSING)`.
# method() looks a method up without calling it, and fails the same way.
cat(show_error(method(describe, class_data.frame)), sep = "\n")
#> Can't find method for `describe(S3<data.frame>)`.
# class_any is the fallback for every class, S7 or not.
method(describe, class_any) <- function(x, ...) {
paste("something of class", class(x)[[1L]])
}
describe("Dune")
#> [1] "something of class character"
method(generic, class) with no assignment looks a method up without calling it, and returns the function, with class S7_method, or fails with the same message as a call. Instead of a class, object = takes a concrete object. When the result of dispatch is a surprise, method_explain() lists the candidate classes in the order S7 tries them, marks with a star the ones that have a registered method, and puts an arrow on the one that will run. Its second argument is a class and not an object. The listing for AudioBook shows the walk up the hierarchy (AudioBook, Book, S7_object) and ends with ANY, the class that class_any registers.
# The method is an ordinary function that can be inspected and called.
m <- method(describe, Book)
class(m)
#> [1] "S7_method" "function" "S7_object"
names(formals(m))
#> [1] "x" "..."
m(dune)
#> [1] "Dune (Frank Herbert, 1965)"
identical(method(describe, object = dune), m)
#> [1] TRUE
# Why was that method chosen? The arrow marks the method that runs and a star
# the other registered methods.
method_explain(describe, AudioBook)
#> -> describe([AudioBook])
#> * describe([Book])
#> describe([S7_object])
#> * describe([ANY])
method_explain(describe, class_character)
#> describe([character])
#> describe([S7_object])
#> -> describe([ANY])
# nolint end
The reference pages of the functions used so far are new_generic, method and S7_dispatch.
Customising the generic
The default body is generated, but new_generic(name, dispatch_args, fun) accepts a function of your own, which must contain a call to S7_dispatch(), usually the last call. There are four reasons to write one:
-
To add a required argument that does not take part in dispatch: give it no default.
-
To add an optional argument: put it after
..., so that callers must name it in full. -
To do some standard work before dispatch, such as checking an argument once for every method.
-
To drop
..., for a low-level generic whose methods must not take extra arguments.
When a method is registered, S7 compares it with the generic. A method that lacks an argument the generic added, or gives it a different default, only produces a warning (the generic may come from another package that is changing its interface), but a method whose first arguments are not the dispatch arguments is an error. Registering a method twice replaces the first and prints a message. The script uses a due() generic with a days argument that defaults to the scenario’s loan_days.
Loan <- new_class("Loan",
properties = list(
loan_id = new_property(class_character, default = NA_character_),
loan_date = new_property(class_Date, default = as.Date(NA))
)
)
loan <- Loan(loan_id = loans$loan_id[[1]], loan_date = loans$loan_date[[1]])
# The body that new_generic() writes for you is one call to S7_dispatch().
due <- new_generic("due", "x")
names(formals(due))
#> [1] "x" "..."
body(due)
#> S7::S7_dispatch()
# Supplying fun lets you add arguments and do standard work before dispatch.
# S7_dispatch() finds the method and calls it with the arguments of the call.
due <- new_generic("due", "x", function(x, ..., days = loan_days) {
stopifnot(is.numeric(days), length(days) == 1L)
S7_dispatch()
})
method(due, Loan) <- function(x, ..., days = loan_days) {
x@loan_date + days
}
due(loan)
#> [1] "2025-02-04"
due(loan, days = 14L)
#> [1] "2025-01-28"
cat(show_error(due(loan, days = "two weeks")), sep = "\n")
#> is.numeric(days) is not TRUE
# A method is checked against its generic when it is registered. A method
# that forgets the extra argument, or changes its default, only gets a warning.
Reservation <- new_class("Reservation",
properties = list(day = new_property(class_Date, default = as.Date(NA)))
)
cat(show_warning(
method(due, Reservation) <- function(x, ...) x@day
), sep = "\n")
#> due(<Reservation>) doesn't have argument `days`
cat(show_warning(
method(due, Reservation) <- function(x, ..., days = 7L) x@day + days
), sep = "\n")
#> In due(<Reservation>), default value of `days` is not the same as the generic
#> - Generic: loan_days
#> - Method: 7L
# The arguments of the method must start with the dispatch argument.
cat(show_error(
method(due, Reservation) <- function(y, ...) y@day
), sep = "\n")
#> due() dispatches on `x`, but due(<Reservation>) has arguments `y`, `...`
# Registering a method twice replaces it, with a message.
withCallingHandlers(
method(due, Loan) <- function(x, ..., days = loan_days) x@loan_date + days,
message = \(m) {
cat(conditionMessage(m))
invokeRestart("muffleMessage")
}
)
#> Overwriting method due(<Loan>)
# A generic without ... rejects arguments the methods did not declare. A call
# that works for one class works for every class.
size <- new_generic("size", "x", function(x) S7_dispatch())
method(size, class_character) <- function(x) sum(nchar(x))
size("Dune")
#> [1] 4
cat(show_error(size("Dune", extra = TRUE)), sep = "\n")
#> unused argument (extra = TRUE)
size
#> <S7_generic> size(x) with 1 methods:
#> 1: method(size, class_character)
# nolint end
Multiple dispatch
A generic with more than one dispatch argument chooses its method from the classes of all of them. new_generic("compare", c("x", "y")) has two, and a method is registered for a list of two classes. class_any matches any class and class_missing an omitted argument, so a three-method generic can cover a specific pair, a specific class and a catch-all, as the script does. The lookup for each argument walks from the class itself up its hierarchy to ANY; the pair that is found first, scanning the first argument’s classes and, within each, the second’s, wins. method_explain() prints the whole search order, which is the way to understand why compare(audio, "Dune") ran the (Book, character) method. S7 chose a deliberately simple, greedy rule instead of the distance metric of S4, and the authors say they do not expect multiple dispatch to be used as much as it is in S4 (S7 package documentation: Generics and methods). A class union (below) in a method signature is shorthand for one method per member class.
Book <- new_class("Book",
properties = list(
title = new_property(class_character, default = NA_character_),
year = new_property(class_integer, default = NA_integer_)
)
)
AudioBook <- new_class("AudioBook",
parent = Book,
properties = list(
minutes = new_property(class_integer, default = NA_integer_)
)
)
# Two dispatch arguments: methods are registered for a list of two classes.
compare <- new_generic("compare", c("x", "y"))
method(compare, list(Book, Book)) <- function(x, y, ...) {
sign(x@year - y@year)
}
method(compare, list(Book, class_character)) <- function(x, y, ...) {
as.integer(x@title == y)
}
method(compare, list(class_any, class_any)) <- function(x, y, ...) {
NA_integer_
}
compare
#> <S7_generic> compare(x, y, ...) with 3 methods:
#> 1: method(compare, list(Book, class_character))
#> 2: method(compare, list(Book, Book))
#> 3: method(compare, list(class_any, class_any))
older <- Book(title = "Emma", year = 1815L)
newer <- Book(title = "Dune", year = 1965L)
audio <- AudioBook(title = "Dune", year = 1965L, minutes = 1260L)
compare(older, newer)
#> [1] -1
compare(newer, audio)
#> [1] 0
compare(audio, "Dune")
#> [1] 1
compare("Dune", audio)
#> [1] NA
compare(1, 2)
#> [1] NA
# class_missing matches an argument that was not supplied.
compare(older)
#> [1] NA
method(compare, list(Book, class_missing)) <- function(x, y, ...) {
"no second book"
}
compare(older)
#> [1] "no second book"
# method_explain() lists the candidates in the order S7 tries them: each
# argument is walked from the class itself up to ANY. The arrow is the method
# that will run; a star marks the other registered methods.
method_explain(compare, list(AudioBook, class_character))
#> compare([AudioBook], [character])
#> compare([AudioBook], [S7_object])
#> compare([AudioBook], [ANY])
#> -> compare([Book], [character])
#> compare([Book], [S7_object])
#> compare([Book], [ANY])
#> compare([S7_object], [character])
#> compare([S7_object], [S7_object])
#> compare([S7_object], [ANY])
#> compare([ANY], [character])
#> compare([ANY], [S7_object])
#> * compare([ANY], [ANY])
# nolint end
The call compare(older) before class_missing has a method falls through to the (ANY, ANY) method, which answers NA; after the method for list(Book, class_missing) exists it is chosen.
print, format and the S3 generics
Printing an S7 object without a method shows the class name and one line per property. To change it, register a method for print, which is an S3 generic: method() accepts S3 generics, S4 generics and S7 generics alike, and for an S3 generic S7 registers an S3 method for the class behind the scenes, so print(x), auto-printing at the console and methods(class = "Book") all see it (S7 package documentation: Compatibility with S3 and S4). The same applies to format(), length(), as.character(), summary() and to the operators, which take two dispatch arguments: a method for == is registered for a list of two classes. Methods for the arithmetic and comparison operators are covered with the group generics of S3 in S3 Group Generics and Operator Overloading.
Book <- new_class("Book",
properties = list(
title = new_property(class_character, default = NA_character_),
author = new_property(class_character, default = NA_character_),
year = new_property(class_integer, default = NA_integer_)
)
)
dune <- Book(title = "Dune", author = "Frank Herbert", year = 1965L)
emma <- Book(title = "Emma", author = "Jane Austen", year = 1815L)
# Without a method, print() shows the class and the properties.
dune
#> <Book>
#> @ title : chr "Dune"
#> @ author: chr "Frank Herbert"
#> @ year : int 1965
print(dune)
#> <Book>
#> @ title : chr "Dune"
#> @ author: chr "Frank Herbert"
#> @ year : int 1965
# format() and friends are S3 generics. method() accepts them like any other
# generic: S7 registers an S3 method for the class behind the scenes.
cat(show_error(format(dune)), sep = "\n")
#> c("Found no format() method for class \"Book\"", "Found no format() method for class \"S7_object\"")
method(format, Book) <- function(x, ...) {
year <- if (is.na(x@year)) "n.d." else x@year
sprintf("%s (%s, %s)", x@title, x@author, year)
}
format(dune)
#> [1] "Dune (Frank Herbert, 1965)"
# print() and the auto-printing of the console use the registered method.
method(print, Book) <- function(x, ...) {
cat("<Book>", format(x), "\n")
invisible(x)
}
dune
#> <Book> Dune (Frank Herbert, 1965)
print(emma)
#> <Book> Emma (Jane Austen, 1815)
methods(class = "Book")
#> [1] format print
#> see '?methods' for accessing help and source code
# Other base generics work the same way.
method(length, Book) <- function(x) 1L
method(as.character, Book) <- function(x, ...) format(x)
method(summary, Book) <- function(object, ...) {
cat("Book:", object@title, "\n")
invisible(object)
}
length(dune)
#> [1] 1
as.character(emma)
#> [1] "Emma (Jane Austen, 1815)"
summary(dune)
#> Book: Dune
The format() error at the start is worth reading. Without a method, format() of an S7 object fails in format.default(), and the message is the character vector of two attempts printed as a single c(...) string, which is what R 4.6.1 prints. Registering the method fixes it, and the print method can then reuse format(), as the S3 print.book of the scenario does. length(), as.character() and summary() are registered the same way, and summary() shows that a method for a generic that is not an S7 generic is called like any S3 method.
# Operators are generics with two dispatch arguments.
method(`==`, list(Book, Book)) <- function(e1, e2) {
e1@title == e2@title && e1@author == e2@author
}
dune == emma
#> [1] FALSE
dune == Book(title = "Dune", author = "Frank Herbert")
#> [1] TRUE
# identical() compares everything; all.equal() on two S7 objects does not look
# at the properties in this version, so do not use it to compare them.
identical(dune, Book(title = "Dune", author = "Frank Herbert", year = 1965L))
#> [1] TRUE
identical(dune, emma)
#> [1] FALSE
all.equal(dune, emma)
#> [1] TRUE
all.equal(props(dune), props(emma))
#> [1] "Component “title”: 1 string mismatch"
#> [2] "Component “author”: 1 string mismatch"
#> [3] "Component “year”: Mean relative difference: 0.07633588"
# super() is for S7 generics. print() is an S3 generic, so inside a print
# method use S7_data() to reach the underlying vector instead.
Pages <- new_class("Pages", parent = class_integer, package = NULL)
method(print, Pages) <- function(x, ...) {
cat("<Pages> ")
print(super(x, to = class_integer))
}
print(Pages(c(387L, 432L)))
#> <Pages> super(<Pages>, <integer>)
method(print, Pages) <- function(x, ...) {
cat("<Pages> ")
print(S7_data(x))
}
print(Pages(c(387L, 432L)))
#> <Pages> [1] 387 432
# nolint end
Source: S7 package documentation: super
Three cautions come out of the last part. First, the method for == decides what "equal" means: it compares titles and authors, so a book without a year equals the one with a year, while identical() compares everything. Second, all.equal() of two S7 objects returned TRUE for objects whose properties differ, on R 4.6.1 with S7 0.2.2, so it must not be used to compare them (identical() and all.equal(props(x), props(y)) do compare the properties; R documentation: all.equal). Third, super() is for S7 generics: inside a method for an S3 generic such as print() it produces a printable "super" object and not the parent’s result, and the recommended way to reach the underlying vector of a class built on a base type is S7_data(), as the last two prints show.
Conversion, unions and base classes
convert(from, to) is the one built-in generic of S7. It dispatches on both arguments, and it is special in two ways: to is a class and not an object, and the class of to does not use inheritance, because a conversion method written for a parent would return parent objects. It has two default methods, down to a parent (the extra properties are dropped) and up to a child (the properties are copied and the new ones are initialised, optionally by extra arguments). Any other conversion is a method on a pair of classes, which replaces the S3 convention of as.foo() generics and the setAs() of S4 (S7 package documentation: convert). The first part of the script converts a Book to a string and a list to a Book.
Book <- new_class("Book",
properties = list(
title = new_property(class_character, default = NA_character_),
author = new_property(class_character, default = NA_character_),
year = new_property(class_integer, default = NA_integer_)
)
)
AudioBook <- new_class("AudioBook",
parent = Book,
properties = list(
narrator = new_property(class_character, default = NA_character_)
)
)
dune <- Book(title = "Dune", author = "Frank Herbert", year = 1965L)
# --- convert(): double dispatch on from and to ------------------------------
# Up and down the class hierarchy it works without any method.
audio <- AudioBook(title = "Dune", narrator = "Scott Brick")
convert(audio, to = Book)
#> <Book>
#> @ title : chr "Dune"
#> @ author: chr NA
#> @ year : int NA
convert(dune, to = AudioBook, narrator = "Scott Brick")
#> <AudioBook>
#> @ title : chr "Dune"
#> @ author : chr "Frank Herbert"
#> @ year : int 1965
#> @ narrator: chr "Scott Brick"
# Everything else needs a method on list(from_class, to_class).
cat(show_error(convert(dune, to = class_character)), sep = "\n")
#> Can't find method for generic `convert()` with dispatch classes:
#> - from: <Book>
#> - to : <character>
#>
method(convert, list(Book, class_character)) <- function(from, to) {
paste0(from@title, " by ", from@author)
}
convert(dune, to = class_character)
#> [1] "Dune by Frank Herbert"
method(convert, list(class_list, Book)) <- function(from, to) {
Book(
title = from$title, author = from$author, year = as.integer(from$year)
)
}
hyperion <- list(title = "Hyperion", author = "Dan Simmons", year = 1989)
convert(hyperion, to = Book)
#> <Book>
#> @ title : chr "Hyperion"
#> @ author: chr "Dan Simmons"
#> @ year : int 1989
A union says that a property or a method signature accepts one of several classes. new_union() builds it and | is the shorthand; NULL | class_character is the idiom for an optional property, whose default is NULL. The default value of a union property is the default of its first member, which is why class_numeric (itself the union of integer and double) gives int(0) for pages in the constructor. In a method, a union is only shorthand for registering the method once for each member; method() cannot look one up (Can't dispatch on unions; must be a concrete type). This differs from S4, where a class union is resolved at dispatch time (S7 package documentation: new_union).
# --- unions ------------------------------------------------------------------
# new_union() or | says "one of these classes". NULL | class_character is an
# optional property; its default is NULL.
Edition <- new_class("Edition",
properties = list(
isbn = NULL | class_character,
pages = class_numeric
)
)
Edition()
#> <Edition>
#> @ isbn : NULL
#> @ pages: int(0)
Edition(isbn = "9780306406157", pages = 412L)
#> <Edition>
#> @ isbn : chr "9780306406157"
#> @ pages: int 412
Edition(pages = 99.5)
#> <Edition>
#> @ isbn : NULL
#> @ pages: num 99.5
cat(show_error(Edition(pages = "many")), sep = "\n")
#> <Edition> object properties are invalid:
#> - @pages must be <integer> or <double>, not <character>
new_union(class_integer, class_character)
#> <S7_union>: <integer> or <character>
S7 ships definitions for the base types and for the S3 classes that R itself uses, all named with a class_ prefix: class_logical, class_integer, class_double, class_complex, class_character, class_raw, class_list, class_expression, class_function, class_environment, class_factor, class_Date, class_POSIXct, class_POSIXlt, class_data.frame, class_formula and a few more, plus the unions class_numeric, class_atomic and class_vector. class_any matches every class and class_missing an omitted argument. They can be used as the type of a property, as the dispatch class of a method, as a member of a union and as the parent of a new class (S7 package documentation: class_any, S7 package documentation: base_classes). The script prints a few of them. The last class, Isbn, has class_character as its parent: its instances are character vectors with S7 behaviour, base functions such as nchar() work on them, S7_data() returns the bare vector, and the validator is written against S7_data(self).
# --- base classes ------------------------------------------------------------
# class_any, class_missing and the class_* objects for base and S3 types.
class_any
#> <S7_any>
class_missing
#> <S7_missing>
class_function
#> <S7_base_class>: <function>
class_list
#> <S7_base_class>: <list>
class_data.frame
#> <S7_S3_class>: S3<data.frame>
class_numeric
#> <S7_union>: <integer> or <double>
class_Date
#> <S7_S3_class>: S3<Date>
# A class can have a base type as its parent. S7_data() gives the bare data.
Isbn <- new_class("Isbn",
parent = class_character,
validator = function(self) {
if (!all(is_valid_isbn13(S7_data(self)))) {
"must be a valid ISBN-13"
}
}
)
isbn <- Isbn("978-0-306-40615-7")
isbn
#> <Isbn> chr "978-0-306-40615-7"
nchar(isbn)
#> [1] 17
S7_data(isbn)
#> [1] "978-0-306-40615-7"
class(isbn)
#> [1] "Isbn" "character" "S7_object"
cat(show_error(Isbn("978-0-306-40615-8")), sep = "\n")
#> <Isbn> object is invalid:
#> - must be a valid ISBN-13
# nolint end
Working with S3
S7 is implemented on top of S3, which is why most interaction with S3 just works (S7 package documentation: Compatibility with S3 and S4). An S7 object has the S3 class attribute, so class(x) is the class vector ("Book", "S7_object", with every parent in between), inherits() works, and a plain S3 method such as summary.Book is found by an S3 generic. The differences from an S3 object are that S7 adds the S7_class attribute that holds the class definition and that its fields are properties, implemented as attributes: code that reads them with attr() still works, but inside S7 code properties should be read with @. When the class belongs to a package, the S3 class name is "package::Class", which is a name that needs backticks in a hand-written S3 method definition (the Pkg class below shows the vector).
An S3 class that has no formal definition, like the book of the scenario, has to be declared before S7 can use it. new_S3_class("book") makes the declaration from the S3 class vector, and it can be used in three places: as the dispatch class of a method, as the type of a property, and as a member of a union. Declaring the class with only its name is enough for those three. What it cannot do is make S7 construct an object of the class: a property of an S3 type with no default makes Shelf(label = "A") fail with S3 class <book> doesn't have a constructor, and extending an S3 class with parent = needs a constructor whose first argument is .data, which is more work and is described in S7 package documentation: new_S3_class. The S7 definition of data.frame that ships with the package, class_data.frame, is such a declaration.
# An S7 object is an S3 object: class() is the S3 class vector.
Book <- new_class("Book",
properties = list(
title = new_property(class_character, default = NA_character_),
year = new_property(class_integer, default = NA_integer_)
)
)
dune <- Book(title = "Dune", year = 1965L)
class(dune)
#> [1] "Book" "S7_object"
inherits(dune, "Book")
#> [1] TRUE
inherits(dune, "S7_object")
#> [1] TRUE
is.object(dune)
#> [1] TRUE
# So an S3 generic finds a plain S3 method for it. The method is an ordinary
# function that happens to use @.
summary.Book <- function(object, ...) {
cat("S3 summary of", object@title, "\n")
invisible(object)
}
summary(dune)
#> S3 summary of Dune
# In a package the S3 class name is "package::class", which needs backticks.
Pkg <- new_class("Pkg", package = "shelf")
class(Pkg())
#> [1] "shelf::Pkg" "S7_object"
Pkg@package
#> [1] "shelf"
# --- using the scenario's S3 book class ------------------------------------
# A plain S3 class has no formal definition, so S7 has to be told about it.
sb <- book("Dune", "Frank Herbert", "978-0-306-40615-7", 1965L, "SF", 412L)
class(sb)
#> [1] "book"
book_s3 <- new_S3_class("book")
book_s3
#> <S7_S3_class>: S3<book>
# 1. As the dispatch class of a method.
describe <- new_generic("describe", "x")
method(describe, book_s3) <- function(x, ...) paste("S3 book:", x$title)
describe(sb)
#> [1] "S3 book: Dune"
cat(show_error(describe(dune)), sep = "\n")
#> Can't find method for `describe(<Book>)`.
# 2. As the type of a property: the value is checked with inherits().
Shelf <- new_class("Shelf",
properties = list(
label = new_property(class_character, default = NA_character_),
first = book_s3
)
)
cat(show_error(Shelf(label = "A")), sep = "\n")
#> S3 class <book> doesn't have a constructor
shelf <- Shelf(label = "A", first = sb)
shelf@first
#> <book> Dune (Frank Herbert, 1965)
cat(show_error(Shelf(label = "A", first = 1)), sep = "\n")
#> <Shelf> object properties are invalid:
#> - @first must be S3<book>, not <double>
cat(show_error(shelf@first <- list(title = "Dune")), sep = "\n")
#> <Shelf>@first must be S3<book>, not <list>
# 3. In a union, with other types.
new_union(book_s3, class_character)
#> <S7_union>: S3<book> or <character>
# Data frames are an S3 class that S7 ships a definition for.
class_data.frame
#> <S7_S3_class>: S3<data.frame>
method(describe, class_data.frame) <- function(x, ...) {
sprintf("a data frame with %d rows", nrow(x))
}
describe(books)
#> [1] "a data frame with 16 rows"
# nolint end
The reverse direction is the one that S3 package authors ask about. If you turn an existing S3 generic into an S7 generic and want the S3 methods that other packages registered to keep working, register a class_any method that calls UseMethod(), so a class without an S7 method falls back to S3 dispatch; the genuine default method is then an ordinary generic.default function (S7 package documentation: Compatibility with S3 and S4). The S3 class system is described in S3 Classes and Generics, and the mechanics of S3 dispatch in The R Language Definition: Object-oriented programming.
Working with S4
S4 classes can take part in S7 in three places, each shown in the next script. An S4 class (its definition from methods::getClass()) can be the dispatch class of an S7 method and the type of an S7 property. For the opposite direction, an S7 method for an S4 generic, the S7 class is first registered with S4 by S4_register(), once, and then method() is used as for any generic. After that the S4 generic keeps its S4 methods next to the S7 one. An S7 object itself is not an S4 object: isS4() is FALSE for it, although methods::is() knows its class.
Book <- new_class("Book",
properties = list(
title = new_property(class_character, default = NA_character_),
year = new_property(class_integer, default = NA_integer_)
)
)
dune <- Book(title = "Dune", year = 1965L)
# Rscript attaches methods, so setClass() and friends are available; the
# methods:: prefix is only for clarity.
methods::setClass("Reader", slots = c(name = "character"))
ana <- methods::new("Reader", name = "Ana Ruiz")
# 1. An S4 class in an S7 method signature: pass its class definition.
greet <- new_generic("greet", "x")
method(greet, methods::getClass("Reader")) <- function(x, ...) {
paste("Hello,", x@name)
}
greet(ana)
#> [1] "Hello, Ana Ruiz"
# 2. An S4 class as the type of an S7 property.
Loan <- new_class("Loan",
properties = list(
member = methods::getClass("Reader"),
book = Book
)
)
loan <- Loan(member = ana, book = dune)
loan@member@name
#> [1] "Ana Ruiz"
loan@book@title
#> [1] "Dune"
# 3. An S7 method for an existing S4 generic: register the S7 class with S4
# once, then use method() as usual.
methods::setGeneric("label", function(x) standardGeneric("label"))
#> [1] "label"
S4_register(Book)
method(label, Book) <- function(x) paste0("[", x@title, "]")
label(dune)
#> [1] "[Dune]"
# 4. The same S4 generic keeps its S4 methods next to the S7 one.
methods::setMethod("label", "Reader", function(x) paste0("<", x@name, ">"))
label(ana)
#> [1] "<Ana Ruiz>"
# The S7 object itself is not an S4 object.
isS4(dune)
#> [1] FALSE
isS4(ana)
#> [1] TRUE
methods::is(dune, "Book")
#> [1] TRUE
# nolint end
Rscript attaches the methods package, so setClass() and friends need no library(methods); the methods:: prefix in the script is for clarity (the S4 system is described in S4 Classes and Methods). S7 properties play the role of S4 slots, with the difference that an S7 property can be computed by a getter while an S4 slot is a stored field with a declared class. Unions behave differently as well: an S4 class union is resolved at dispatch time, an S7 union at the time a method is registered. The package documentation describes further interoperability that the development version has (an S7 class that extends an S4 class, and an S4 class that extends an S7 class); it is not part of 0.2.2 and is not used here.
S7 in a package
The packages article of the S7 site collects what is different when classes and generics are defined in a package instead of a script (S7 package documentation: Using S7 in a package). The checklist, for 0.2.2:
-
Add
S7to theImportsfield ofDESCRIPTION, and import its functions into the namespace (import(S7)inNAMESPACE, or@import S7with roxygen2). -
Call
S7::methods_register()in the package’s.onLoad(). S7 registers methods when the package is loaded, not withexportdirectives inNAMESPACEthe way S3 and S4 do. The call is strictly necessary only when the package registers methods for generics defined in other packages, but there is no harm in adding it, and it keeps the next change from forgetting it. -
Export the class and the generics that users should call: a class is exported by exporting its constructor, which is the object returned by
new_class(). If a class is exported, set itspackageargument so that classes with the same name in different packages are told apart; inside a packagenew_class()fills it in. -
Define subclasses after their parents and methods after both the class and the generic. When the definitions live in several files, the
Collatefield ofDESCRIPTION(or the roxygen2@includetag) fixes the loading order. -
To register methods for generics (or classes) from a package that is only suggested, use
new_external_generic()(the article of the site also namesnew_external_class()for classes, which 0.2.2 does not export) so that the method is registered when that package is loaded. -
S7 properties are read with
@. Before R 4.3.0@only works for S4 objects, so a package that supports older R versions has to useprop()or conditionally import S7’s@; requiring R 4.3.0 or later withDepends: R (>= 4.3.0)avoids the problem.
The skeleton below is a fragment, not a script: it is an excerpt of DESCRIPTION and of R/zzz.R, and it cannot be run on its own because it needs a package to be loaded (outside one, methods_register() fails with not a namespace, which the second script shows). The DESCRIPTION stores the roxygen2 version in Config/roxygen2/version (roxygen2 8.x; older versions used a different field), and Config/roxygen2/markdown: TRUE turns on the Markdown syntax in comments.
Package: shelf
Title: A Library Shelf
Version: 0.1.0
Depends: R (>= 4.3.0)
Imports: S7
Config/roxygen2/markdown: TRUE
Config/roxygen2/version: 8.1.1
Collate:
'book.R'
'describe.R'
'zzz.R'
# R/zzz.R of the shelf package (a fragment, not run by this page).
.onLoad <- function(...) {
S7::methods_register()
}
With roxygen2, a class is documented like its constructor function and a generic like any other function: @param documents the arguments, which are the properties unless the constructor was customised, @returns the object, and @export makes the name visible. A computed property that is not an argument can be described with @prop. Methods need not be documented one by one and need no @export, because methods_register() registers them; a method with its own behaviour can get its own block with a link back to the generic (the roxygen2 article is roxygen2 package documentation: Documenting S7; roxygen2 8.1.1 was installed for this page and ships that vignette). The general workflow of writing, documenting and exporting is in Creating Packages and Documentation with Rd, roxygen2 and Vignettes, and namespaces in Packages, Libraries and Namespaces.
The script below runs the part of this that can be run at the console: new_external_generic() describes stats::median without importing it, a method is registered for a class of this page, and stats::median() finds it. Its last lines show the error of methods_register() outside a package.
BookList <- new_class("BookList",
properties = list(years = class_integer)
)
# A generic that lives in another package (stats::median) without importing it:
# describe it with new_external_generic() and register a method as usual.
median_generic <- new_external_generic("stats", "median", "x")
median_generic
#> <S7_external_generic> stats::median(x)
method(median_generic, BookList) <- function(x, na.rm = FALSE, ...) {
stats::median(x@years, na.rm = na.rm)
}
shelf <- BookList(years = c(1813L, 1974L, 1999L, 2021L, NA))
stats::median(shelf, na.rm = TRUE)
#> [1] 1986.5
median(shelf, na.rm = TRUE)
#> [1] 1986.5
# An external generic is a small list that records where the generic lives.
class(median_generic)
#> [1] "S7_external_generic"
is.list(median_generic)
#> [1] TRUE
median_generic$package
#> [1] "stats"
median_generic$name
#> [1] "median"
median_generic$dispatch_args
#> [1] "x"
# methods_register() belongs in the .onLoad() of a package. Outside a package
# namespace there is nothing to register for, and it fails.
tryCatch(methods_register(), error = \(e) conditionMessage(e))
#> [1] "not a namespace"
# nolint end
The book in S3, S4, R6 and S7
The sibling pages of this section model the same book of the shelf scenario with the same six fields in the same order (title, author, isbn, year, genre, pages) and the same validation rules as validate_book(): the S3 book of S3 Classes and Generics, the S4 class Book of S4 Classes and Methods, the Reference Class BookRC and the R6 class BookR6 of Reference Classes and R6, and the S7 class Book of this page. The next script builds a trimmed version (title, author and year) of each of S3, S4, R6 and S7, and of a Reference Class, in one session and asks the questions on which they differ in behaviour: is an empty title rejected when the object is built, when a field is assigned afterwards, is a value of the wrong type rejected, what happens to the original when a copy is modified, and what do class(), typeof() and isS4() say. The S4 class is called BookS4 there only so that it can live next to the S7 Book in the same session.
library(R6)
library(S7)
source("scenario.R")
# nolint start: object_name_linter.
# The same book in five systems (Reference Classes are added to the four of
# the table), trimmed to title, author and year. The S4
# class is called BookS4 here only so that it can coexist with the S7 Book.
is_ok <- function(expr) {
tryCatch({
expr
"accepted"
}, error = \(e) "rejected")
}
# S3: the scenario's book() helper.
s3 <- book("Dune", "Frank Herbert", year = 1965L)
# S4
methods::setClass("BookS4",
slots = c(title = "character", author = "character", year = "integer"),
validity = function(object) {
if (!nzchar(object@title)) "a book needs a title" else TRUE
}
)
s4 <- methods::new(
"BookS4",
title = "Dune", author = "Frank Herbert", year = 1965L
)
# R6
BookR6 <- R6Class("BookR6",
public = list(
title = NULL, author = NULL, year = NULL,
initialize = function(title, author = NA_character_, year = NA_integer_) {
if (!nzchar(title)) {
stop("a book needs a title", call. = FALSE)
}
self$title <- title
self$author <- author
self$year <- year
}
)
)
r6 <- BookR6$new("Dune", "Frank Herbert", 1965L)
# Reference Classes
BookRC <- setRefClass("BookRC",
fields = list(title = "character", author = "character", year = "integer")
)
rc <- BookRC$new(title = "Dune", author = "Frank Herbert", year = 1965L)
# S7
Book <- new_class("Book",
properties = list(
title = new_property(class_character, default = NA_character_),
author = new_property(class_character, default = NA_character_),
year = new_property(class_integer, default = NA_integer_)
),
validator = function(self) {
if (is.na(self@title) || !nzchar(self@title)) "a book needs a title"
}
)
s7 <- Book(title = "Dune", author = "Frank Herbert", year = 1965L)
# 1. Is an empty title rejected at construction?
c(
S3 = is_ok(book("")),
S4 = is_ok(methods::new("BookS4", title = "")),
R6 = is_ok(BookR6$new("")),
RC = is_ok(BookRC$new(title = "")),
S7 = is_ok(Book(title = ""))
)
#> S3 S4 R6 RC S7
#> "rejected" "rejected" "rejected" "accepted" "rejected"
# 2. Is an empty title rejected when a field is assigned afterwards?
c(
S3 = is_ok({
x <- s3
x$title <- ""
}),
S4 = is_ok({
x <- s4
x@title <- ""
}),
R6 = is_ok({
x <- r6$clone()
x$title <- ""
}),
RC = is_ok({
x <- rc$copy()
x$title <- ""
}),
S7 = is_ok({
x <- s7
x@title <- ""
})
)
#> S3 S4 R6 RC S7
#> "accepted" "accepted" "accepted" "accepted" "rejected"
# 3. Is a value of the wrong type rejected at assignment?
c(
S3 = is_ok({
x <- s3
x$year <- "1965"
}),
S4 = is_ok({
x <- s4
x@year <- "1965"
}),
R6 = is_ok({
x <- r6$clone()
x$year <- "1965"
}),
RC = is_ok({
x <- rc$copy()
x$year <- "1965"
}),
S7 = is_ok({
x <- s7
x@year <- "1965"
})
)
#> S3 S4 R6 RC S7
#> "accepted" "rejected" "accepted" "rejected" "rejected"
# 4. Copy semantics: change a copy, then look at the original.
copy_s3 <- s3
copy_s3$title <- "Emma"
copy_s4 <- s4
copy_s4@title <- "Emma"
copy_r6 <- r6
copy_r6$title <- "Emma"
copy_rc <- rc
copy_rc$title <- "Emma"
copy_s7 <- s7
copy_s7@title <- "Emma"
c(S3 = s3$title, S4 = s4@title, R6 = r6$title, RC = rc$title, S7 = s7@title)
#> S3 S4 R6 RC S7
#> "Dune" "Dune" "Emma" "Emma" "Dune"
# 5. What do class(), typeof() and isS4() say?
class(s3)
#> [1] "book"
class(s4)
#> [1] "BookS4"
#> attr(,"package")
#> [1] ".GlobalEnv"
class(r6)
#> [1] "BookR6" "R6"
class(rc)
#> [1] "BookRC"
#> attr(,"package")
#> [1] ".GlobalEnv"
class(s7)
#> [1] "Book" "S7_object"
c(
S3 = typeof(s3), S4 = typeof(s4), R6 = typeof(r6), RC = typeof(rc),
S7 = typeof(s7)
)
#> S3 S4 R6 RC S7
#> "list" "S4" "environment" "S4" "object"
c(S3 = isS4(s3), S4 = isS4(s4), R6 = isS4(r6), RC = isS4(rc), S7 = isS4(s7))
#> S3 S4 R6 RC S7
#> FALSE TRUE FALSE TRUE FALSE
# nolint end
Source: R documentation: typeof
The table is the summary. Each cell is short on purpose, and the details are on the page of the system. The Reference Class BookRC of the script has no column. Like R6 it has reference semantics (the modified copy changed the original) and does not validate values, but it checks the class of a field at assignment (the year = "1965" assignment was rejected), and its objects are S4 objects (typeof() is "S4").
| S3 | S4 | R6 | S7 | |
|---|---|---|---|---|
Definition call |
None; the class is a |
|
|
|
Constructor |
A function you write ( |
|
|
The class object, |
Field access |
|
|
|
|
Validation: when it runs |
Only in code you call ( |
Types and the |
Only what |
At construction, at every |
Method definition |
|
|
Inside the class: |
|
Dispatch |
Single, on the first argument; |
Single or multiple, on a signature |
None: the method belongs to the object, |
Single or multiple, |
Inheritance |
Class vector, |
|
|
|
Mutability and copies |
Value semantics (copy on modify) |
Value semantics |
Reference semantics; |
Value semantics |
Printing hook |
|
A |
A |
|
Where it lives |
Base R |
The |
CRAN package R6 (2.6.1 here) |
CRAN package S7 (0.2.2 here); not in base R |
When to choose it |
Simple classes, maximum compatibility, no dependency |
Formal classes, multiple dispatch, a mature base-R system (Bioconductor) |
Mutable state with methods: caches, connections, Shiny |
Formal classes with typed, validated, computed properties and clear dispatch, when an add-on package is acceptable |
Reading the table row by row, the S7 column differs from S3 in that classes are defined and validated, from S4 in that the validation also runs at assignment and the dispatch rule is simpler, and from R6 in that objects are values and methods belong to generics. The choice is not exclusive: S7 interoperates with S3 and S4, and the plot object of ggplot2 4.0.3 is an S7 object that is also an S3 object.
Pitfalls collected
-
The type check is strict about integers and doubles:
year = 1965is rejected with@year must be <integer>, not <double>; use1965Lor coerce in a custom constructor. -
A property without
defaultstarts empty, with length zero, so a validator that wants length one rejects the default object: give every property adefault. -
The name given to
new_class()andnew_generic()should be the name of the variable the result is assigned to; the string is what the S3 class vector and the messages use. -
@does not match names partially and a misspelt property is an error,Can't find property <Book>@subtitle, but the constructor reports a misspelt argument as the plain R errorunused argument. -
method(generic, class) <- fneeds the first arguments offto be the dispatch arguments, otherwise registration is an error; a missing or different extra argument is only a warning, so read warnings frommethod<-. -
A call with no matching method says
Can't find method forthe generic and the dispatch class.class_anyis the explicit fallback. -
super()works in S7 generics and does not work in S3 generics such asprint(); useS7_data()there. -
all.equal()on two S7 objects returnedTRUEfor different properties with R 4.6.1 and S7 0.2.2; compareprops()or useidentical(). -
format()of an S7 object without a method fails insideformat.default()with an unusual message; register a method forformat. -
A class union cannot be the target of
method()lookup and cannot be dispatched on as a concrete type: the lookup saysCan't dispatch on unions; must be a concrete type. -
methods_register()belongs in a package.onLoad(); at the console it fails withnot a namespace. -
The
:=operator and some features of the development documentation (the S4 class helpers of the compatibility article,new_external_class()) are not in 0.2.2. CheckpackageVersion("S7")before copying code from the site.
Version notes
| Version | Change | Source |
|---|---|---|
R 4.3.0 |
The |
R NEWS 4.3.0 |
R 4.4.0 |
The internal type |
R NEWS 4.4.0 |
S7 0.1.0 |
First CRAN release, 2023-08-24. |
S7 changelog |
S7 0.1.1 |
2023-09-17. |
S7 changelog |
S7 0.2.0 |
2024-11-07. The default constructor accepts lazy property defaults and includes dynamic properties with a setter; all custom setters are invoked by the default constructor; |
S7 changelog |
ggplot2 4.0.0 |
CRAN release 2025-09-11. "The S3 parts of ggplot2 have been replaced with S7 bits." The image has ggplot2 4.0.3. |
ggplot2 NEWS |
S7 0.2.1 |
2025-11-14. |
S7 changelog |
S7 0.2.2 |
2026-04-22. The current CRAN release and the version used on this page; the release contains internal changes for R-devel (4.6). |
S7 changelog |
R 4.6.1 (this page) |
S7 is not part of base R: |
This page, first script |
The R NEWS entries are in the release NEWS and the ggplot2 entry is in the ggplot2 NEWS. The changelog page of the S7 site also lists changes that have not been released; this table lists only the released versions, and the installed version was checked with packageVersion("S7").
Where older tutorials differ
| Older tutorial | Current practice | Why | Version |
|---|---|---|---|
|
|
R7 was the earlier name of the project; the package is now S7 |
S7 0.1.0 onwards |
"S7 will be in R 4.x" or "S7 is part of base R" |
Install it from CRAN, attach it or use |
merging S7 into base R is a long-term goal, not a released fact |
R 4.6.1 |
|
|
the |
S7 0.2.2 |
|
class objects: |
types are S7, S3 or S4 class objects, and strings are refused |
S7 0.2.2 |
A class validator that checks every property, because a per-property validator is missing |
|
the argument was added in 0.1.1 |
S7 0.1.1 |
Properties read with |
|
|
always |
|
|
the parent class is named where the call is written |
S7 |
A hand-written |
|
S7 registers its methods at load time |
S7 0.2.2 |
"S4 is the modern formal system and S3 is informal" |
S4 is mature, remains part of base R and is not deprecated; S7 is the successor that the R Consortium working group designed and that ggplot2 4.0 adopted |
S7 is a package that is still described as somewhat experimental |
R 4.6.1 |
ggplot2 tutorials that treat a plot as an S3 list ( |
a ggplot2 4.0.x plot is an S7 object: |
the S3 parts of ggplot2 were replaced with S7 bits |
ggplot2 4.0.0 |
For the full list of changes by release see What’s New and Migrating Older Code.
Where to go next
-
OOP Overview and Base Types for
typeof(),class(), implicit classes,OBJSXPand how the five systems relate. -
S3 Classes and Generics and S3 Group Generics and Operator Overloading for the system S7 is built on and for operator methods.
-
S4 Classes and Methods for the formal system in base R that S7 interoperates with.
-
Reference Classes and R6 for objects with reference semantics.
-
Attributes and Names for the attributes that S7 properties are built on.
-
Defensive Programming and Input Validation and Conditions and Error Handling for validation in functions and for catching the errors shown above.
-
Creating Packages, Documentation with Rd, roxygen2 and Vignettes and Testing with testthat for putting classes into a package.
-
ggplot2 and the Grammar of Graphics for the package whose plot objects are S7 objects.
-
Memory and Copy Semantics for the copy-on-modify rule that S7 objects follow.
References
-
R NEWS: Changes in R 4.3.0 and 4.4.0