The S7 package provides a new OOP system designed to be a successor to S3 and S4. It has been designed and implemented collaboratively by the RConsortium Object-Oriented Programming Working Group, which includes representatives from R-Core, BioConductor, RStudio/tidyverse, and the wider R community.
This vignette gives an overview of the most important parts of S7: classes and objects, generics and methods, and the basics of method dispatch and inheritance.
S7 classes have a formal definition that you create with
new_class()
. There are two arguments that you’ll use with
almost every class:
name
of the class, supplied in the first
argument.properties
, the data associated with each
instance of the class. The easiest way to define properties is to supply
a named list where the values define the valid types of the
property.The following code defines a simple dog
class with two
properties: a character name
and a numeric
age
.
dog <- new_class("dog", properties = list(
name = class_character,
age = class_numeric
))
dog
#> <dog> class
#> @ parent : <S7_object>
#> @ constructor: function(name, age) {...}
#> @ validator : <NULL>
#> @ properties :
#> $ name: <character>
#> $ age : <integer> or <double>
S7 provides a number of built-in definitions that allow you to refer
to existing base types that are not S7 classes. You can recognize these
definitions because they all start with class_
.
Note that I’ve assigned the return value of new_class()
to an object with the same name as the class. This is important! That
object represents the class and is what you use to construct instances
of the class:
Once you have an S7 object, you can get and set properties using
@
:
S7 automatically validates the type of the property using the type
supplied in new_class()
:
Given an object, you can retrieves its class
S7_class()
:
S7_class(lola)
#> <dog> class
#> @ parent : <S7_object>
#> @ constructor: function(name, age) {...}
#> @ validator : <NULL>
#> @ properties :
#> $ name: <character>
#> $ age : <integer> or <double>
S7 objects also have an S3 class()
. This is used for
compatibility with existing S3 generics and you can learn more about it
in vignette("compatibility")
.
If you want to learn more about the details of S7 classes and
objects, including validation methods and more details of properties,
please see vignette("classes-objects")
.
S7, like S3 and S4, is built around the idea of generic functions, or generics for short. A generic defines an interface, which uses a different implementation depending on the class of one or more arguments. The implementation for a specific class is called a method, and the generic finds that appropriate method by performing method dispatch.
Use new_generic()
to create a S7 generic. In its
simplest form, it only needs two arguments: the name of the generic
(used in error messages) and the name of the argument used for method
dispatch:
Like with new_class()
, you should always assign the
result of new_generic()
to a variable with the same name as
the first argument.
Once you have a generic, you can register methods for specific
classes with
method(generic, class) <- implementation
.
Once the method is registered, the generic will use it when appropriate:
Let’s define another class, this one for cats, and define another
method for speak()
:
cat <- new_class("cat", properties = list(
name = class_character,
age = class_double
))
method(speak, cat) <- function(x) {
"Meow"
}
fluffy <- cat(name = "Fluffy", age = 5)
speak(fluffy)
#> [1] "Meow"
You get an error if you call the generic with a class that doesn’t have a method:
The cat
and dog
classes share the same
properties, so we could use a common parent class to extract out the
duplicated specification. We first define the parent class:
Then use the parent
argument to
new_class:
cat <- new_class("cat", parent = pet)
dog <- new_class("dog", parent = pet)
cat
#> <cat> class
#> @ parent : <pet>
#> @ constructor: function(name, age) {...}
#> @ validator : <NULL>
#> @ properties :
#> $ name: <character>
#> $ age : <integer> or <double>
dog
#> <dog> class
#> @ parent : <pet>
#> @ constructor: function(name, age) {...}
#> @ validator : <NULL>
#> @ properties :
#> $ name: <character>
#> $ age : <integer> or <double>
Because we have created new classes, we need to recreate the existing
lola
and fluffy
objects:
Method dispatch takes advantage of the hierarchy of parent classes: if a method is not defined for a class, it will try the method for the parent class, and so on until it finds a method or gives up with an error. This inheritance is a powerful mechanism for sharing code across classes.
describe <- new_generic("describe", "x")
method(describe, pet) <- function(x) {
paste0(x@name, " is ", x@age, " years old")
}
describe(lola)
#> [1] "Lola is 11 years old"
describe(fluffy)
#> [1] "Fluffy is 5 years old"
method(describe, dog) <- function(x) {
paste0(x@name, " is a ", x@age, " year old dog")
}
describe(lola)
#> [1] "Lola is a 11 year old dog"
describe(fluffy)
#> [1] "Fluffy is 5 years old"
You can define a fallback method for any S7 object by registering a
method for S7_object
:
method(describe, S7_object) <- function(x) {
"An S7 object"
}
cocktail <- new_class("cocktail",
properties = list(
ingredients = class_character
)
)
martini <- cocktail(ingredients = c("gin", "vermouth"))
describe(martini)
#> [1] "An S7 object"
Printing a generic will show you which methods are currently defined:
describe
#> <S7_generic> describe(x, ...) with 3 methods:
#> 1: method(describe, pet)
#> 2: method(describe, dog)
#> 3: method(describe, S7_object)
And you can use method()
to retrieve the implementation
of one of those methods:
method(describe, pet)
#> <S7_method> method(describe, pet)
#> function (x)
#> {
#> paste0(x@name, " is ", x@age, " years old")
#> }
#> <bytecode: 0x119e2fdb0>
Learn more about method dispatch in
vignette("generics-methods")
.