R Object-Oriented Programming
S7, S3, S4, and vctrs: choosing the right OOP system for your needs
S7: Modern OOP for New Projects
- S7 combines S3 simplicity with S4 structure
- Formal class definitions with automatic validation
- Compatible with existing S3 code
# S7 class definition
Range <- new_class("Range",
properties = list(
start = class_double,
end = class_double
),
validator = function(self) {
if (self@end < self@start) {
"@end must be >= @start"
}
}
)
# Usage - constructor and property access
x <- Range(start = 1, end = 10)
x@start # 1
x@end <- 20 # automatic validation
# Methods
inside <- new_generic("inside", "x")
method(inside, Range) <- function(x, y) {
y >= x@start & y <= x@end
}
OOP System Decision Matrix
S7 vs vctrs vs S3/S4 Decision Tree
Start here: What are you building?
1. Vector-like objects (things that behave like atomic vectors)
Use vctrs when:
- Need data frame integration (columns/rows)
- Want type-stable vector operations
- Building factor-like, date-like, or numeric-like classes
- Need consistent coercion/casting behavior
- Working with existing tidyverse infrastructure
Examples: custom date classes, units, categorical data
2. General objects (complex data structures, not vector-like)
Use S7 when:
- NEW projects that need formal classes
- Want property validation and safe property access (@)
- Need multiple dispatch (beyond S3's double dispatch)
- Converting from S3 and want better structure
- Building class hierarchies with inheritance
- Want better error messages and discoverability
Use S3 when:
- Simple classes with minimal structure needs
- Maximum compatibility and minimal dependencies
- Quick prototyping or internal classes
- Contributing to existing S3-based ecosystems
- Performance is absolutely critical (minimal overhead)
Use S4 when:
- Working in Bioconductor ecosystem
- Need complex multiple inheritance (S7 doesn't support this)
- Existing S4 codebase that works well
Detailed S7 vs S3 Comparison
| Feature |
S3 |
S7 |
When S7 wins |
| Class definition |
Informal (convention) |
Formal (new_class()) |
Need guaranteed structure |
| Property access |
$ or attr() (unsafe) |
@ (safe, validated) |
Property validation matters |
| Validation |
Manual, inconsistent |
Built-in validators |
Data integrity important |
| Method discovery |
Hard to find methods |
Clear method printing |
Developer experience matters |
| Multiple dispatch |
Limited (base generics) |
Full multiple dispatch |
Complex method dispatch needed |
| Inheritance |
Informal, NextMethod() |
Explicit super() |
Predictable inheritance needed |
| Migration cost |
- |
Low (1-2 hours) |
Want better structure |
| Performance |
Fastest |
~Same as S3 |
Performance difference negligible |
| Compatibility |
Full S3 |
Full S3 + S7 |
Need both old and new patterns |
Practical Guidelines
Choose S7 when you have
# Complex validation needs
Range <- new_class("Range",
properties = list(start = class_double, end = class_double),
validator = function(self) {
if (self@end < self@start) "@end must be >= @start"
}
)
# Multiple dispatch needs
method(generic, list(ClassA, ClassB)) <- function(x, y) ...
# Class hierarchies with clear inheritance
Child <- new_class("Child", parent = Parent)
Choose vctrs when you need
# Vector-like behavior in data frames
percent <- new_vctr(0.5, class = "percentage")
data.frame(x = 1:3, pct = percent(c(0.1, 0.2, 0.3))) # works seamlessly
# Type-stable operations
vec_c(percent(0.1), percent(0.2)) # predictable behavior
vec_cast(0.5, percent()) # explicit, safe casting
Choose S3 when you have
# Simple classes without complex needs
new_simple <- function(x) structure(x, class = "simple")
print.simple <- function(x, ...) cat("Simple:", x)
# Maximum performance needs (rare)
# Existing S3 ecosystem contributions
S3 Patterns
Basic S3 Class
# Constructor
new_person <- function(name, age) {
stopifnot(is.character(name), length(name) == 1)
stopifnot(is.numeric(age), length(age) == 1)
structure(
list(name = name, age = age),
class = "person"
)
}
# Print method
print.person <- function(x, ...) {
cat("Person:", x$name, "(age", x$age, ")\n")
invisible(x)
}
# Generic + method
greet <- function(x) UseMethod("greet")
greet.person <- function(x) {
cat("Hello, my name is", x$name, "\n")
}
greet.default <- function(x) {
cat("Hello!\n")
}
S3 Inheritance
# Child class
new_employee <- function(name, age, company) {
obj <- new_person(name, age)
obj$company <- company
class(obj) <- c("employee", class(obj))
obj
}
# Method with inheritance
print.employee <- function(x, ...) {
NextMethod() # Call parent print method
cat("Works at:", x$company, "\n")
invisible(x)
}
S7 Patterns
Basic S7 Class
library(S7)
# Define class
Person <- new_class("Person",
properties = list(
name = class_character,
age = class_numeric
),
validator = function(self) {
if (self@age < 0) {
"@age must be non-negative"
}
}
)
# Create instance
bob <- Person(name = "Bob", age = 30)
bob@name # "Bob"
bob@age <- 31 # Validated assignment
S7 Methods
# Define generic
greet <- new_generic("greet", "x")
# Add method
method(greet, Person) <- function(x) {
cat("Hello, my name is", x@name, "\n")
}
# Default method
method(greet, class_any) <- function(x) {
cat("Hello!\n")
}
S7 Inheritance
Employee <- new_class("Employee",
parent = Person,
properties = list(
company = class_character
)
)
# Override method
method(greet, Employee) <- function(x) {
super(x, Person)@greet() # Call parent method
cat("I work at", x@company, "\n")
}
S7 Multiple Dispatch
# Generic with multiple dispatch
combine <- new_generic("combine", c("x", "y"))
# Method for specific combination
method(combine, list(Person, Person)) <- function(x, y) {
cat(x@name, "meets", y@name, "\n")
}
method(combine, list(Person, class_character)) <- function(x, y) {
cat(x@name, "receives message:", y, "\n")
}
Migration Strategy
- S3 -> S7: Usually 1-2 hours work, keeps full compatibility
- S4 -> S7: More complex, evaluate if S4 features are actually needed
- Base R -> vctrs: For vector-like classes, significant benefits
- Combining approaches: S7 classes can use vctrs principles internally
Migration Example: S3 to S7
# Original S3
new_person_s3 <- function(name, age) {
structure(list(name = name, age = age), class = "person")
}
# Migrated S7
Person <- new_class("Person",
properties = list(
name = class_character,
age = class_numeric
)
)
# S7 is backwards compatible with S3 generics
# Existing S3 methods still work
When NOT to Use OOP
Sometimes simpler approaches are better:
# Don't create a class for simple data
# BAD
Point <- new_class("Point", properties = list(x = class_double, y = class_double))
# GOOD - just use a named list or vector
point <- c(x = 1.5, y = 2.3)
# Don't create classes for one-off operations
# Use functions instead
distance <- function(p1, p2) {
sqrt((p1["x"] - p2["x"])^2 + (p1["y"] - p2["y"])^2)
}
1---2name: r-oop3description: R object-oriented programming guide for S7, S3, S4, and vctrs. Use when mentions "orientado a objetos", "orientado a objetos em R", "object-oriented", "object-oriented in R", "OOP", "OOP in R", "OOP em R", "POO", "POO em R", "classes", "classes em R", "classes in R", "métodos", "métodos em R", "methods", "methods in R", "S3 class", "S4 class", "S7 class", "S7", "classe S3", "classe S4", "classe S7", "sistema S3", "sistema S4", "sistema S7", "S3 system", "S4 system", "S7 system", "method dispatch", "despacho de métodos", "dispatch de métodos", "generic functions", "funções genéricas", "generics", "genéricos", "inheritance", "herança", "vctrs", "vctrs package", "methods", "methods package", "setClass", "setGeneric", "setMethod", "new_class", "new_generic", "new_property", "criar classe", "create class", "design class", "desenhar classe", "definir classe", "define class", "sistema de objetos", "object system", "class system", "sistema de classes", "definir métodos", "define methods", "criar genéricos", "create g4---56# R Object-Oriented Programming78*S7, S3, S4, and vctrs: choosing the right OOP system for your needs*910## S7: Modern OOP for New Projects1112- **S7 combines S3 simplicity with S4 structure**13- **Formal class definitions with automatic validation**14- **Compatible with existing S3 code**1516```r17# S7 class definition18Range <- new_class("Range",19 properties = list(20 start = class_double,21 end = class_double22 ),23 validator = function(self) {24 if (self@end < self@start) {25 "@end must be >= @start"26 }27 }28)2930# Usage - constructor and property access31x <- Range(start = 1, end = 10)32x@start # 133x@end <- 20 # automatic validation3435# Methods36inside <- new_generic("inside", "x")37method(inside, Range) <- function(x, y) {38 y >= x@start & y <= x@end39}40```4142## OOP System Decision Matrix4344### S7 vs vctrs vs S3/S4 Decision Tree4546**Start here:** What are you building?4748### 1. Vector-like objects (things that behave like atomic vectors)4950```51Use vctrs when:52- Need data frame integration (columns/rows)53- Want type-stable vector operations54- Building factor-like, date-like, or numeric-like classes55- Need consistent coercion/casting behavior56- Working with existing tidyverse infrastructure5758Examples: custom date classes, units, categorical data59```6061### 2. General objects (complex data structures, not vector-like)6263```64Use S7 when:65- NEW projects that need formal classes66- Want property validation and safe property access (@)67- Need multiple dispatch (beyond S3's double dispatch)68- Converting from S3 and want better structure69- Building class hierarchies with inheritance70- Want better error messages and discoverability7172Use S3 when:73- Simple classes with minimal structure needs74- Maximum compatibility and minimal dependencies75- Quick prototyping or internal classes76- Contributing to existing S3-based ecosystems77- Performance is absolutely critical (minimal overhead)7879Use S4 when:80- Working in Bioconductor ecosystem81- Need complex multiple inheritance (S7 doesn't support this)82- Existing S4 codebase that works well83```8485## Detailed S7 vs S3 Comparison8687| Feature | S3 | S7 | When S7 wins |88|---------|----|----|---------------|89| **Class definition** | Informal (convention) | Formal (`new_class()`) | Need guaranteed structure |90| **Property access** | `$` or `attr()` (unsafe) | `@` (safe, validated) | Property validation matters |91| **Validation** | Manual, inconsistent | Built-in validators | Data integrity important |92| **Method discovery** | Hard to find methods | Clear method printing | Developer experience matters |93| **Multiple dispatch** | Limited (base generics) | Full multiple dispatch | Complex method dispatch needed |94| **Inheritance** | Informal, `NextMethod()` | Explicit `super()` | Predictable inheritance needed |95| **Migration cost** | - | Low (1-2 hours) | Want better structure |96| **Performance** | Fastest | ~Same as S3 | Performance difference negligible |97| **Compatibility** | Full S3 | Full S3 + S7 | Need both old and new patterns |9899## Practical Guidelines100101### Choose S7 when you have102103```r104# Complex validation needs105Range <- new_class("Range",106 properties = list(start = class_double, end = class_double),107 validator = function(self) {108 if (self@end < self@start) "@end must be >= @start"109 }110)111112# Multiple dispatch needs113method(generic, list(ClassA, ClassB)) <- function(x, y) ...114115# Class hierarchies with clear inheritance116Child <- new_class("Child", parent = Parent)117```118119### Choose vctrs when you need120121```r122# Vector-like behavior in data frames123percent <- new_vctr(0.5, class = "percentage")124data.frame(x = 1:3, pct = percent(c(0.1, 0.2, 0.3))) # works seamlessly125126# Type-stable operations127vec_c(percent(0.1), percent(0.2)) # predictable behavior128vec_cast(0.5, percent()) # explicit, safe casting129```130131### Choose S3 when you have132133```r134# Simple classes without complex needs135new_simple <- function(x) structure(x, class = "simple")136print.simple <- function(x, ...) cat("Simple:", x)137138# Maximum performance needs (rare)139# Existing S3 ecosystem contributions140```141142## S3 Patterns143144### Basic S3 Class145146```r147# Constructor148new_person <- function(name, age) {149 stopifnot(is.character(name), length(name) == 1)150 stopifnot(is.numeric(age), length(age) == 1)151152 structure(153 list(name = name, age = age),154 class = "person"155 )156}157158# Print method159print.person <- function(x, ...) {160 cat("Person:", x$name, "(age", x$age, ")\n")161 invisible(x)162}163164# Generic + method165greet <- function(x) UseMethod("greet")166greet.person <- function(x) {167 cat("Hello, my name is", x$name, "\n")168}169greet.default <- function(x) {170 cat("Hello!\n")171}172```173174### S3 Inheritance175176```r177# Child class178new_employee <- function(name, age, company) {179 obj <- new_person(name, age)180 obj$company <- company181 class(obj) <- c("employee", class(obj))182 obj183}184185# Method with inheritance186print.employee <- function(x, ...) {187 NextMethod() # Call parent print method188 cat("Works at:", x$company, "\n")189 invisible(x)190}191```192193## S7 Patterns194195### Basic S7 Class196197```r198library(S7)199200# Define class201Person <- new_class("Person",202 properties = list(203 name = class_character,204 age = class_numeric205 ),206 validator = function(self) {207 if (self@age < 0) {208 "@age must be non-negative"209 }210 }211)212213# Create instance214bob <- Person(name = "Bob", age = 30)215bob@name # "Bob"216bob@age <- 31 # Validated assignment217```218219### S7 Methods220221```r222# Define generic223greet <- new_generic("greet", "x")224225# Add method226method(greet, Person) <- function(x) {227 cat("Hello, my name is", x@name, "\n")228}229230# Default method231method(greet, class_any) <- function(x) {232 cat("Hello!\n")233}234```235236### S7 Inheritance237238```r239Employee <- new_class("Employee",240 parent = Person,241 properties = list(242 company = class_character243 )244)245246# Override method247method(greet, Employee) <- function(x) {248 super(x, Person)@greet() # Call parent method249 cat("I work at", x@company, "\n")250}251```252253### S7 Multiple Dispatch254255```r256# Generic with multiple dispatch257combine <- new_generic("combine", c("x", "y"))258259# Method for specific combination260method(combine, list(Person, Person)) <- function(x, y) {261 cat(x@name, "meets", y@name, "\n")262}263264method(combine, list(Person, class_character)) <- function(x, y) {265 cat(x@name, "receives message:", y, "\n")266}267```268269## Migration Strategy2702711. **S3 -> S7**: Usually 1-2 hours work, keeps full compatibility2722. **S4 -> S7**: More complex, evaluate if S4 features are actually needed2733. **Base R -> vctrs**: For vector-like classes, significant benefits2744. **Combining approaches**: S7 classes can use vctrs principles internally275276### Migration Example: S3 to S7277278```r279# Original S3280new_person_s3 <- function(name, age) {281 structure(list(name = name, age = age), class = "person")282}283284# Migrated S7285Person <- new_class("Person",286 properties = list(287 name = class_character,288 age = class_numeric289 )290)291292# S7 is backwards compatible with S3 generics293# Existing S3 methods still work294```295296## When NOT to Use OOP297298Sometimes simpler approaches are better:299300```r301# Don't create a class for simple data302# BAD303Point <- new_class("Point", properties = list(x = class_double, y = class_double))304305# GOOD - just use a named list or vector306point <- c(x = 1.5, y = 2.3)307308# Don't create classes for one-off operations309# Use functions instead310distance <- function(p1, p2) {311 sqrt((p1["x"] - p2["x"])^2 + (p1["y"] - p2["y"])^2)312}313```