Modifying R6 objects after creation - Part 2

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.