We’re excited to announce that S7 v0.2.0 is now available on CRAN! S7 is a new object-oriented programming (OOP) system designed to supersede both S3 and S4. You might wonder why R needs a new OOP system when we already have two. The reason lies in the history of R’s OOP journey: S3 is a simple and effective system for single dispatch, while S4 adds formal class definitions and multiple dispatch, but at the cost of complexity. This has forced developers to choose between the simplicity of S3 and the sophistication of S4.
The goal of S7 is to unify the OOP landscape by building on S3’s existing dispatch system and incorporating the most useful features of S4 (along with some new ones), all with a simpler syntax. S7’s design and implementation have been a collaborative effort by a working group from the R Consortium, including representatives from R-Core, Bioconductor, tidyverse/Posit, ROpenSci, and the wider R community. Since S7 builds on S3, it is fully compatible with existing S3-based code. It’s also been thoughtfully designed to work with S4, and as we learn more about the challenges of transitioning from S4 to S7, we’ll continue to add features to ease this process.
Our long-term goal is to include S7 in base R, but for now, you can install it from CRAN:
install.packages("S7")
What’s new in the second release
The second release of S7 brings refinements and bug fixes. Highlights include:
- Support for lazy property defaults, making class setup more flexible.
- Custom property setters now run on object initialization.
- Significant speed improvements for setting and getting properties with
@
and@<-
. - Expanded compatibility with base S3 classes.
-
convert()
now provides a default method for transforming a parent class into a subclass.
Additionally, there are numerous bug fixes and quality-of-life improvements, such as better error messages, improved support for base Ops methods, and compatibility improvements for using @
in R versions prior to 4.3. You can see a full list of changes in the
release notes.
Who should use S7
S7 is a great fit for R users who like to try new things but don’t need to be the first. It’s already used in several CRAN packages, and the tidyverse team is applying it in new projects. While you may still run into a few issues, many early problems have been resolved.
Usage
Let’s dive into the basics of S7. To learn more, check out the package vignettes, including a more detailed introduction in
vignette("S7")
, and coverage of generics and methods in
vignette("generics-methods")
, and classes and objects in
vignette("classes-objects")
.
Classes and objects
S7 classes have formal definitions, specified by
new_class()
, which includes a list of properties and an optional validator. For example, the following code creates a Range
class with start
and end
properties, and a validator to ensure that start
is always less than end
:
Range <- new_class("Range",
properties = list(
start = class_double,
end = class_double
),
validator = function(self) {
if (length(self@start) != 1) {
"@start must be length 1"
} else if (length(self@end) != 1) {
"@end must be length 1"
} else if (self@end < self@start) {
"@end must be greater than or equal to @start"
}
}
)
new_class()
returns the class object, which also serves as the constructor to create instances of the class:
x <- Range(start = 1, end = 10)
x
#> <Range>
#> @ start: num 1
#> @ end : num 10
Properties
The data an object holds are called its properties. Use @
to get and set properties:
x@start
#> [1] 1
x@end <- 20
x
#> <Range>
#> @ start: num 1
#> @ end : num 20
Properties are automatically validated against the type declared in
new_class()
(in this case, double
) and checked by the class validator:
x@end <- "x"
#> Error: <Range>@end must be <double>, not <character>
x@end <- -1
#> Error: <Range> object is invalid:
#> - @end must be greater than or equal to @start
Generics and methods
Like S3 and S4, S7 uses functional OOP, where methods belong to generic functions, and method calls look like regular function calls: generic(object, arg2, arg3)
. A generic uses the types of its arguments to automatically pick the appropriate method implementation.
You can create a new generic with
new_generic()
, specifying the arguments to dispatch on:
inside <- new_generic("inside", "x")
To define a method for a specific class, use method(generic, class) <- implementation
:
method(inside, Range) <- function(x, y) {
y >= x@start & y <= x@end
}
inside(x, c(0, 5, 10, 15))
#> [1] FALSE TRUE TRUE TRUE
Printing the generic shows its methods:
inside
#> <S7_generic> inside(x, ...) with 1 methods:
#> 1: method(inside, Range)
And you can retrieve the method for a specific class:
method(inside, Range)
#> <S7_method> method(inside, Range)
#> function (x, y)
#> {
#> y >= x@start & y <= x@end
#> }
Known limitations
While we are pleased with S7’s design, there are still some limitations:
- S7 objects can be serialized to disk (with
saveRDS()
), but the current implementation saves the entire class specification with each object. This may change in the future. - Support for implicit S3 classes
"array"
and"matrix"
is still in development.
We expect the community will uncover more issues as S7 is more widely adopted. If you encounter any problems, please file an issue at https://github.com/RConsortium/OOP-WG/issues. We appreciate your feedback in helping us make S7 even better! 😃
Acknowledgements
Thank you to all people who have contributed issues, code, and comments to this release:
@calderonsamuel, @Crosita, @DavisVaughan, @dipterix, @guslipkin, @gvelasq, @hadley, @jeffkimbrel, @jl5000, @jmbarbone, @jmiahjones, @jonthegeek, @JosiahParry, @jtlandis, @lawremi, @MarcellGranat, @mikmart, @mmaechler, @mynanshan, @rikivillalba, @sjcowtan, @t-kalinowski, @teunbrand, and @waynelapierre.