| name | r-oop |
| description | R object-oriented programming guide for S7, S3, S4, and vctrs. Use when designing R classes or choosing an OOP system. |
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
Range <- new_class("Range",
properties = list(
start = class_double,
end = class_double
),
validator = function(self) {
if (self@end < self@start) {
"@end must be >= @start"
}
}
)
x <- Range(start = 1, end = 10)
x@start
x@end <- 20
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
Range <- new_class("Range",
properties = list(start = class_double, end = class_double),
validator = function(self) {
if (self@end < self@start) "@end must be >= @start"
}
)
method(generic, list(ClassA, ClassB)) <- function(x, y) ...
Child <- new_class("Child", parent = Parent)
Choose vctrs when you need
percent <- new_vctr(0.5, class = "percentage")
data.frame(x = 1:3, pct = percent(c(0.1, 0.2, 0.3)))
vec_c(percent(0.1), percent(0.2))
vec_cast(0.5, percent())
Choose S3 when you have
new_simple <- function(x) structure(x, class = "simple")
print.simple <- function(x, ...) cat("Simple:", x)
S3 Patterns
Basic S3 Class
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.person <- function(x, ...) {
cat("Person:", x$name, "(age", x$age
x
greet x
greet.person x
cat xname
greet.default x
cat
S3 Inheritance
new_employee <- function(name, age, company) {
obj <- new_person(name, age)
obj$company <- company
class(obj) <- c("employee", class(obj))
obj
}
print.employee <- function(x, ...) {
NextMethod()
cat("Works at:", x$company, "\n")
invisible(x)
}
S7 Patterns
Basic S7 Class
library(S7)
Person <- new_class("Person",
properties = list(
name = class_character,
age = class_numeric
),
validator = function(self) {
if (self@age < 0) {
"@age must be non-negative"
}
}
)
bob <- Person(name = "Bob", age = 30)
bob@name
bob@age <- 31
S7 Methods
greet <- new_generic("greet", "x")
method(greet, Person) <- function(x) {
cat("Hello, my name is", x@name, "\n")
}
method(greet, class_any) <- function(x) {
cat("Hello!\n")
}
S7 Inheritance
Employee <- new_class("Employee",
parent = Person,
properties = list(
company = class_character
)
)
method(greet, Employee) <- function(x) {
super(x, Person)@greet()
cat("I work at", x@company, "\n")
}
S7 Multiple Dispatch
combine <- new_generic("combine", c("x", "y"))
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
new_person_s3 <- function(name, age) {
structure(list(name = name, age = age), class = "person")
}
Person <- new_class("Person",
properties = list(
name = class_character,
age = class_numeric
)
)
When NOT to Use OOP
Sometimes simpler approaches are better:
Point <- new_class("Point", properties = list(x = class_double, y = class_double))
point <- c(x = 1.5, y = 2.3)
distance <- function(p1, p2) {
sqrt((p1["x"] - p2["x"])^2 + (p1["y"] - p2["y"])^2)