R6 Inheritance, Private Methods & Active Bindings: Advanced R6
R6 supports single inheritance with inherit, private methods for internal logic, active bindings that look like fields but run code, and a finalize method for cleanup. These features make R6 suitable for production-grade R packages.
This tutorial assumes you already know the basics of R6 (see R6 Classes in R). Here we go deeper into the features that make R6 a serious OOP tool.
Inheritance with inherit and super
Use inherit in R6Class() to create a child class. The child inherits all public and private fields/methods. Use super$ to call parent methods.
library(R6)
Base <- R6Class("Base",
public = list(
id = NULL,
initialize = function(id) self$id <- id,
info = function() cat("Base id:", self$id, "\n")
)
)
Middle <- R6Class("Middle",
inherit = Base,
public = list(
level = "middle",
initialize = function(id) {
super$initialize(id)
},
info = function() {
super$info()
cat("Level:", self$level, "\n")
}
)
)
Top <- R6Class("Top",
inherit = Middle,
public = list(
tag = NULL,
initialize = function(id, tag) {
super$initialize(id)
self$tag <- tag
},
info = function() {
super$info()
cat("Tag:", self$tag, "\n")
}
)
)
obj <- Top$new("A1", "production")
obj$info()
cat("Inherits from Base?", inherits(obj, "Base"), "\n")
Private Methods
Private methods encapsulate internal logic that users of the class should not call directly.
library(R6)
PasswordManager <- R6Class("PasswordManager",
public = list(
initialize = function() {
private$passwords <- list()
},
add = function(site, password) {
if (!private$is_strong(password)) {
stop("Password too weak! Must be >= 8 chars with a digit.")
}
private$passwords[[site]] <- private$hash(password)
cat("Password saved for", site, "\n")
invisible(self)
},
verify = function(site, password) {
stored <- private$passwords[[site]]
if (is.null(stored)) {
cat("No password stored for", site, "\n")
return(FALSE)
}
result <- stored == private$hash(password)
cat("Verification for", site, ":", if (result) "PASS" else "FAIL", "\n")
result
}
),
private = list(
passwords = NULL,
is_strong = function(pw) {
nchar(pw) >= 8 && grepl("[0-9]", pw)
},
hash = function(pw) {
# Simple hash simulation (use real hashing in production!)
paste0("hashed_", nchar(pw), "_", substr(pw, 1, 1))
}
)
)
pm <- PasswordManager$new()
pm$add("github", "MyPass123!")
pm$verify("github", "MyPass123!")
pm$verify("github", "wrong")
Active Bindings
Active bindings look like fields but execute code when read or written. They are perfect for computed properties, validation on assignment, and read-only fields.
By default, R6 classes are portable (portable = TRUE). This means you use self$ and private$ explicitly. Non-portable classes bind methods into the object's environment, so you can access fields without self$. Stick with portable (the default) -- it's clearer.
library(R6)
# Portable (default, recommended)
Portable <- R6Class("Portable",
portable = TRUE,
public = list(
x = 10,
show_x = function() cat("x =", self$x, "\n") # Need self$
)
)
Portable$new()$show_x()
# Non-portable (legacy style)
NonPortable <- R6Class("NonPortable",
portable = FALSE,
public = list(
x = 10,
show_x = function() cat("x =", x, "\n") # No self$ needed
)
)
NonPortable$new()$show_x()
Summary Table
Feature
Syntax
Purpose
Inheritance
R6Class("Child", inherit = Parent)
Single inheritance
Call parent
super$method()
Invoke parent method
Private methods
private = list(fn = function() ...)
Internal-only logic
Active bindings
active = list(prop = function(value) ...)
Computed properties
Finalize
private = list(finalize = function() ...)
Cleanup on GC
Portable
portable = TRUE (default)
Explicit self$/private$
Lock class
lock_class = TRUE
Prevent adding new fields
Practice Exercises
Exercise 1: Create a Shape base class with an area() method. Then create Circle and Square child classes that override area(). Both should call super$initialize().
Click to reveal solution
```r
library(R6)
Shape <- R6Class("Shape",
public = list(
color = NULL,
initialize = function(color = "black") self$color <- color,
area = function() stop("area() not implemented"),
describe = function() cat(self$color, class(self)[1], "area:", self$area(), "\n")
)
)
Circle <- R6Class("Circle", inherit = Shape,
public = list(
radius = NULL,
initialize = function(radius, color = "red") {
super$initialize(color)
self$radius <- radius
},
area = function() pi * self$radius^2
)
)
Square <- R6Class("Square", inherit = Shape,
public = list(
side = NULL,
initialize = function(side, color = "blue") {
super$initialize(color)
self$side <- side
},
area = function() self$side^2
)
)
Circle$new(5)$describe()
Square$new(4)$describe()
Exercise 2: Create a RangedValue class with an active binding value that clamps any assignment between min and max (set in the constructor).
Q: Does R6 support multiple inheritance? No. R6 supports only single inheritance (one parent). If you need to share behavior across unrelated classes, use composition (include another R6 object as a field) rather than inheritance.
Q: What is the difference between active bindings and getter/setter methods? Active bindings use property syntax (obj$prop and obj$prop <- val) while getters/setters use method syntax (obj$get_prop() and obj$set_prop(val)). Active bindings are more natural and Pythonic.
Q: Should I always use finalize for cleanup? Only when your object holds external resources (file handles, database connections, temp files). For normal R objects, garbage collection handles memory automatically.