S7 Classes

code
analysis
The new simpler R classes
Author

Zhenglei Gao

Published

February 4, 2025

During today’s R Validation Hub working group meeting, I learned that there is this new S7 class in R and it seems simpler than S7.

I asked grok to tell me about this S/ class. Here is what I got.

S7 is a new object-oriented programming (OOP) system in R, designed to be a successor to the older S3 and S4 systems. It aims to combine the simplicity of S3 with the formal structure of S4, offering a more robust and flexible framework for creating classes, objects, and methods in R. Below is an overview of S7 classes, their key components, and how they work.


What Are S7 Classes?

S7 classes provide a formal way to define objects in R, with a focus on structured data, validation, and method dispatch. Unlike S3, which is informal and relies on naming conventions, or S4, which is complex and rigid, S7 offers a balance by providing a clear syntax, type safety, and compatibility with existing R systems.

  • Purpose: S7 is designed to unify R’s OOP landscape, making it easier to define classes with properties, validate data, and create generic functions with methods tailored to specific classes.
  • Development: S7 is developed by the R Consortium Object-Oriented Programming Working Group, including contributors from R-Core, Bioconductor, Posit/tidyverse, and the broader R community. Its long-term goal is to be integrated into base R.
  • Availability: S7 is available as a CRAN package (install.packages("S7")) or from GitHub for the development version (remotes::install_github("rconsortium/OOP-WG")).

Key Concepts of S7 Classes

  1. Classes:
    • A class in S7 is a blueprint for creating objects, defining their structure (properties) and behavior (methods).

    • Classes are created using the new_class() function, which requires:

      • A name (string) for the class.
      • A parent class (defaults to S7_object for top-level classes).
      • A list of properties (data associated with the class).
      • An optional validator function to enforce constraints.
      • An optional constructor for custom object creation.
      • An abstract flag to prevent instantiation (if TRUE).
    • Example:

      library(S7)
      Dog <- new_class("Dog",
        properties = list(
          name = class_character,
          age = class_numeric
        )
      )
      lola <- Dog(name = "Lola", age = 11)
      lola  #> <Dog> @ name: chr "Lola" @ age: num 11

      This defines a Dog class with name (character) and age (numeric) properties.

  2. Objects:
    • Objects are instances of a class, created using the class’s constructor (automatically generated by new_class() or customized).

    • Properties are accessed and modified using the @ operator:

      lola@name  # Get property: "Lola"
      lola@age <- 12  # Set property
    • S7 objects have copy-on-modify semantics, meaning changes create a new copy rather than modifying the original (unlike R6 or Reference Classes, which use reference semantics).

  3. Properties:
    • Properties are the data associated with a class, defined with specific types (e.g., class_character, class_numeric) for automatic type validation.

    • Properties can include:

      • Type constraints: Ensure properties conform to specified types (e.g., class_double for doubles).
      • Validators: Custom functions to check property values.
      • Getters/Setters: Custom functions to control access or modification.
      • Dynamic/Computed Properties: Properties that are calculated on-the-fly (e.g., points on a circle based on radius).
    • Example with validation:

      Range <- new_class("Range",
        properties = list(
          start = class_numeric,
          end = class_numeric
        ),
        validator = function(self) {
          if (length(self@start) != 1) "@start must be a single number"
          else if (length(self@end) != 1) "@end must be a single number"
          else if (self@end < self@start) "@end must be >= @start"
        }
      )
      r <- Range(start = 10, end = 20)  # Valid
      try(Range(start = 20, end = 10))  # Error: @end must be >= @start

  4. Generics and Methods:
    • S7 uses functional OOP, where methods belong to generic functions (like S3 and S4), not directly to classes (unlike R6).

    • Generics are created with new_generic(), specifying the name and dispatch arguments:

      describe <- new_generic("describe", "x")
      method(describe, Dog) <- function(x) {
        paste0(x@name, " is a ", x@age, " year old dog")
      }
      describe(lola)  #> "Lola is a 11 year old dog"
    • S7 supports single and limited multiple dispatch, allowing methods to vary based on the class of one or more arguments.

  5. Inheritance:
    • S7 supports single inheritance, where a class can inherit properties and methods from a parent class.

    • Example:

      Pet <- new_class("Pet",
        properties = list(name = class_character, age = class_numeric)
      )
      Dog <- new_class("Dog", parent = Pet)
      lola <- Dog(name = "Lola", age = 11)
    • Methods defined for the parent class (Pet) can be used by child classes (Dog) unless overridden.

  6. Validators:
    • Validators ensure objects maintain valid states, checking property values during initialization or modification.
    • They return NULL if valid or a character vector of error messages if invalid.
  7. Compatibility:
    • S7 is designed to work with S3 and S4 systems:
      • S7 classes can extend S3 classes using new_S3_class().
      • S7 methods can be registered for S3 or S4 generics, and vice versa.
      • However, S7 classes cannot extend S4 classes directly, which may require more effort for S4-heavy codebases.
    • S7 objects include an S3 class attribute for compatibility with existing S3 generics.

Key Features and Benefits

  • Formal Class Definitions: Unlike S3, S7 classes are defined explicitly with properties and types, reducing errors from invalid objects.
  • Type Safety: Properties are validated against declared types, and custom validators can enforce additional constraints.
  • Simpler Syntax than S4: S7 avoids S4’s complex syntax, making it more accessible while retaining structure.
  • Dynamic Properties: Properties can be computed dynamically, updating automatically when dependencies change.
  • Functional OOP: Methods are tied to generics, maintaining R’s functional programming style but with better organization.
  • Performance Improvements: Recent updates (e.g., S7 v0.2.0) enhance speed for property access and modification.
  • Community-Driven: Developed collaboratively, S7 is designed to meet diverse needs in the R community.

Limitations and Considerations

  • Copy-on-Modify Semantics: S7 objects are immutable in the sense that modifications create copies, which may impact performance for large objects compared to R6 or Reference Classes (which use reference semantics).
  • Experimental Status: S7 is still considered experimental, and changes may occur before it’s integrated into base R.
  • S4 Extension Limitation: S7 classes cannot directly extend S4 classes, which may complicate transitions for S4-heavy projects.
  • Learning Curve: While simpler than S4, S7 introduces new concepts (e.g., @ for property access) that may require adjustment for S3 users.

Comparison with Other R OOP Systems

  • S3: Simple and flexible but lacks formal class definitions and validation. S7 builds on S3’s dispatch system with added structure.
  • S4: Formal but complex, with rigid syntax and side-effect-based definitions. S7 simplifies this while retaining key features.
  • R6: Uses encapsulated OOP (methods belong to objects), with reference semantics for in-place modification. S7’s functional OOP and copy-on-modify semantics differ, making R6 better for mutable objects but less integrated with R’s functional style.
  • Reference Classes (RC): Similar to R6 but less adopted and clunkier. S7 is more modern and aligned with R’s ecosystem.

Use Cases

S7 is ideal for: - Projects requiring structured objects with type safety and validation (e.g., data analysis pipelines, scientific modeling). - Developers transitioning from S3 who want more robustness without S4’s complexity. - New R packages aiming for modern OOP practices, as seen in some CRAN packages adopting S7.

Example use case: A hydrological model (HBV) where S7 classes define structured objects with validated properties (e.g., runoff, date) and methods for computation.


Getting Started

To try S7: 1. Install the package: R install.packages("S7") 2. Explore the vignettes for detailed guides: R browseVignettes("S7") 3. Check the GitHub repository for updates and examples: https://github.com/RConsortium/S7.


Example: Full Workflow

library(S7)

# Define a parent class
Pet <- new_class("Pet",
  properties = list(
    name = class_character,
    age = class_numeric
  )
)

# Define a child class
Dog <- new_class("Dog", parent = Pet)

# Create a generic function
describe <- new_generic("describe", "x")

# Define methods
method(describe, Pet) <- function(x) {
  paste0(x@name, " is ", x@age, " years old")
}
method(describe, Dog) <- function(x) {
  paste0(x@name, " is a ", x@age, " year old dog")
}

# Create an object
lola <- Dog(name = "Lola", age = 11)

# Use the method
describe(lola)  #> "Lola is a 11 year old dog"

This demonstrates class creation, inheritance, method dispatch, and property access.


Conclusion

S7 classes offer a modern, flexible, and structured approach to OOP in R, blending the best of S3 and S4 while introducing new features like dynamic properties and improved validation. While still experimental, S7 is gaining traction in the R community and is a promising tool for developers seeking robust OOP capabilities. For more details, refer to the S7 vignettes or Hadley Wickham’s “Advanced R” for foundational OOP context (though it predates S7).