R5 Reference Classes in R: setRefClass() -- Legacy OOP
R5 Reference Classes (also called "R5" or just "Reference Classes") were R's first built-in system for mutable, reference-semantics objects. They use setRefClass() and are part of base R. However, the R6 package has largely replaced them for new code due to better performance and simpler syntax.
You'll encounter R5 in older packages and Bioconductor code. This tutorial covers the essentials so you can read and maintain R5 code, while explaining why R6 is the better choice for new projects.
Why R6 Is Preferred Over R5
Feature
R5 (Reference Classes)
R6
Speed
Slower (S4 machinery underneath)
Faster
Syntax
More verbose
Cleaner
Dependencies
Base R
R6 package (zero deps)
Active bindings
No
Yes
Private fields
Via locking (awkward)
Native support
Deep clone
Manual
Built-in
Community
Declining
Growing
The main reason to learn R5 is to read legacy code. For new projects, use R6.
Basic R5 Class Definition
# Define a Reference Class
Person <- setRefClass("Person",
fields = list(
name = "character",
age = "numeric"
),
methods = list(
initialize = function(name, age) {
name <<- name # <<- assigns to the field, not a local variable
age <<- age
},
greet = function() {
cat("Hi, I'm", name, "and I'm", age, "years old.\n")
},
have_birthday = function() {
age <<- age + 1 # Modifies in place (reference semantics)
cat(name, "is now", age, "\n")
}
)
)
# Create an instance
alice <- Person$new("Alice", 30)
alice$greet()
alice$have_birthday()
alice$greet()
Note the <<- operator inside methods. In R5, methods run in their own environment, so you must use <<- to modify the object's fields (not <-, which would create a local variable).
Reference Semantics (Same as R6)
Like R6, R5 objects use reference semantics. Assignment does not copy.
Counter <- setRefClass("Counter",
fields = list(
count = "numeric"
),
methods = list(
initialize = function() {
count <<- 0
},
increment = function() {
count <<- count + 1
}
)
)
c1 <- Counter$new()
c2 <- c1 # NOT a copy!
c1$increment()
c1$increment()
cat("c1:", c1$count, "\n") # 2
cat("c2:", c2$count, "\n") # Also 2! Same object.
# Use $copy() for an independent copy
c3 <- c1$copy()
c1$increment()
cat("c1:", c1$count, "\n") # 3
cat("c3:", c3$count, "\n") # Still 2
Exercise 1: Create an R5 Stack class with push(val), pop(), and size() methods.
Click to reveal solution
```r
Stack <- setRefClass("Stack",
fields = list(
elements = "list"
),
methods = list(
initialize = function() {
elements <<- list()
},
push = function(val) {
elements[[length(elements) + 1]] <<- val
},
pop = function() {
if (length(elements) == 0) stop("Stack empty")
val <- elements[[length(elements)]]
elements[[length(elements)]] <<- NULL
val
},
size = function() {
length(elements)
}
)
)
s <- Stack$new()
s$push(10)
s$push(20)
cat("Size:", s$size(), "\n")
cat("Pop:", s$pop(), "\n")
cat("Size:", s$size(), "\n")
Exercise 2: Convert the Stack class above to R6.
Click to reveal solution
```r
library(R6)
Stack <- R6Class("Stack",
public = list(
initialize = function() {
private$elements <- list()
},
push = function(val) {
private$elements[[length(private$elements) + 1]] <- val
invisible(self)
},
pop = function() {
n <- length(private$elements)
if (n == 0) stop("Stack empty")
val <- private$elements[[n]]
private$elements[[n]] <- NULL
val
},
size = function() length(private$elements)
),
private = list(
elements = NULL
)
)
s <- Stack$new()
s$push(10)$push(20)
cat("Size:", s$size(), "\n")
cat("Pop:", s$pop(), "\n")
FAQ
Q: Is R5 deprecated? No. R5 is part of base R and will continue to work. It's just not the recommended choice for new code. R6 is faster, simpler, and has a more active community.
Q: Can R5 and R6 objects interact? Yes. They're just R objects. You can pass an R5 object as an argument to an R6 method and vice versa. They don't interoperate at the class level (no cross-system inheritance), but they coexist fine.
Q: Why does R5 use <<- instead of self$? R5 methods run in a special environment where fields are in the parent environment. <<- assigns to the parent environment (the object's fields). This is confusing, which is one reason R6 switched to explicit self$.