R6 classes
The {R6}
package is an implementation of encapsulated object-oriented programming for R, and is a simpler, faster, lighter-weight alternative to R’s built-in reference classes. This style of programming is also sometimes referred to as classical object-oriented programming.
In this post I’ll be using the word class
to refer to the original class definition
using R6::R6Class()
and object
to refer to an instantiation of that class
e.g. my_object = ClassName$new()
.
In this post, I’m experimenting with overriding the $
operator to automatically
add variables rather than return NULL
when trying to access variables which
do not exist in the class defintion.
Current R6 Behaviour when Reading Missing Member Variables
If you attempt to read a variable which does not exist, then R6 will return NULL.
MyClass <- R6::R6Class(
"MyClass",
lock_object = FALSE,
public = list(height = 100)
)
# Create an instance of the class
myobj <- MyClass$new()
# Call a method on this object
myobj$height
## [1] 100
# Try and call a non-existant method and get a NULL
myobj$name
## NULL
Override the $
operator to add missing variables rather than return NULL
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# OVerride $ to check for existance of element. If not, then add to object
# with a default value of 999
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`$.MyClass` <- function(x, y) {
if (!y %in% names(x)) {
x[[y]] <- 999
}
NextMethod()
}
myobj$name
## [1] 999
Automatically, Dynamicalliy creating nested/linked objects
This is an experimental node/tree class that automatically creates new node objects when one doesn’t exist at the named location in an R6 object.
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# A node class
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Node <- R6::R6Class(
"Node",
lock_object = FALSE,
public = list(
index = NULL,
initialize = function(index = 1L) {self$index = index; self},
print = function() {cat("I am a node at depth:", self$index, "\n")}
)
)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# OVerride $ to check for existance of element. If not, then add a new
# node object at this location
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`$.Node` <- function(x, y) {
if (!y %in% names(x)) {
message("Creating new node at depth: ", x[['index']] + 1L)
x[[y]] <- Node$new(x[['index']] + 1L)
}
NextMethod()
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Create a single new object
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
node <- Node$new(1)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Every reference to an unknown variable in that object creates a new
# 'Node' object at that location
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
node$node2
## Creating new node at depth: 2
## I am a node at depth: 2
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Node creation can be chained together!
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
node$node2$node3$node4$node5$node6
## Creating new node at depth: 3
## Creating new node at depth: 4
## Creating new node at depth: 5
## Creating new node at depth: 6
## I am a node at depth: 6
What’s next?
This means I can establish a base class with a certain set of methods, but I can now dynamically create a menagerie of slightly different objects which each have different sets of methods and variables.
I guess this is sort of similar to perhaps defining a class inheritance hierarchy except what I want to do is regularly, and dynamically, modify the methods available on existing objects during runtime.