Fun With R6 Classes

R has several object-oriented systems and I'm a big fan of R6. Detailed below is a specific use-case. I wanted a parent class that held a list of child classes with thet specification that the child class instances could update the parent class instance.

Parent Class

The parent class is shown below along with a table detailing the public and private fields and methods. the purpose of the parent class is to hold a series of steps along with methods to interact with them. In addition, the parent class has a private field called accumulator which we will update from the child classes.

public/private field/method description
public name a label
public initialize() create a new instance
public update(n) update the accumulator by n (default n = 1)
public count() return the value of the accumulator
public add(step) add a new step to the parent class (steps are child classes)
public run() execute all the steps (child classes)
public status() return the status of each step
private accumulator an accumulator, intially set to 0
private steps list of steps
 1#' R6 parent class
 2parent_class <- R6::R6Class("parent_class",
 3                            public = list(
 4
 5                              #' @field name Class label
 6                              name = "",
 7
 8                              #' @description
 9                              #' Initialize the class
10                              initialize = function(name) {
11                                self$name = name
12                                invisible(self)
13                              },
14
15                              #' @description
16                              #' Update accumulator by value
17                              update = function(n = 1) {
18                                private$accumulator <- private$accumulator + n
19                              },
20
21                              #' @description
22                              #' Return the value of the accumulator
23                              count = function() {
24                                return(private$accumulator)
25                              },
26
27                              #' @description
28                              #' Add a new step
29                              #' @param step type of step to add
30                              add = function(step) {
31                                new_name <- paste0(sample(LETTERS, size = 8), collapse = "")
32                                new_step <- get(step)$new(name = new_name)
33                                private$steps[[new_name]] <- new_step
34                              },
35
36                              #' @description
37                              #' Run the steps
38                              run = function() {
39                                for (s in private$steps) {
40                                  s$execute(parent = self)
41                                }
42                              },
43
44                              #' @description
45                              #' Return status of steps
46                              status = function() {
47                                lapply(private$steps, function(s) {
48                                  list(name = s$name, value = s$val, status = s$status)
49                                }) |> dplyr::bind_rows()
50                              }
51
52                            ),
53
54                            private = list(
55                              accumulator = 0,
56                              steps = list()
57                            )
58)

Child Class - Generic

For child classes we first build a generic class that can manage any function that is common across the child classes. We can then use the property of inheritance so that the generic child class methods are available for all child classes, adding any specific methods. The generic class is shown below along with a list of public fields and methods.

field/method description
name a label
val numeric to store a class value (intial = NA)
status status notification - possible values are initialized and run
initialize() create a new instance
execute() execute the class - set val equal to parent$count() and change status to run
 1child_class <- R6::R6Class("child_class",
 2                           public = list(
 3
 4                             #'  @field name class label
 5                             name = NULL,
 6
 7                             #' @field val class value
 8                             val = NA,
 9
10                             #' @field status class status
11                             status = "initialized",
12
13                             #' @description
14                             #' Initialize class
15                             initialize = function(name) {
16                               self$name <- name
17                             },
18
19                             #' @description
20                             #' Execute the class.  Set internal value equal to the
21                             #'     parent class `accumulator`
22                             #' @param parent Parent class
23                             execute = function(parent) {
24                               self$val <- parent$count()
25                               self$status <- "run"
26                             }
27                           )
28)

Child Class - Child Classes

We define two child classes. The first increases the parent accumulator field by one, and the second doubles it. Each child class inherits the generic class to avoid repetition. The only change from the generic class is the public execute() method.

field/method description
execute() execute the class - set val equal to parent$count(), change parent accumulator according to the step, and change status to run
 1step_add_one <- R6::R6Class("step_add_one",
 2
 3                            inherit = child_class,
 4
 5                            public = list(
 6
 7                              #' @description
 8                              #' Execute the class.  Set internal value equal to the
 9                              #'     parent class `accumulator` and increase the parent
10                              #'     class `accumulator` by 1.
11                              #' @param parent Parent class
12                              execute = function(parent) {
13                                self$val <- parent$count()
14                                parent$update()
15                                self$status <- "run"
16                              }
17                            )
18)
 1step_double <- R6::R6Class("step_double",
 2
 3                           inherit = child_class,
 4
 5                           public = list(
 6
 7                             #' @description
 8                             #' Execute the class.  Set internal value equal to the
 9                             #'     parent class `accumulator` and multiply the parent
10                             #'     class `accumulator` by 2.
11                             #' @param parent Parent class
12                             execute = function(parent) {
13                               self$val <- parent$count()
14                               parent$update(n = parent$count())
15                               self$status <- "run"
16                             }
17                           )
18)

Execution

 1my_parent <- parent_class$new('parent class')
 2my_parent$add('step_add_one')
 3my_parent$add('step_add_one')
 4my_parent$add('step_double')
 5my_parent$add('step_double')
 6my_parent$count()
 7[1] 0
 8
 9my_parent$status()
10# A tibble: 4 × 3
11  name     value status     
12  <chr>    <lgl> <chr>      
131 UWCITAHO NA    initialized
142 KCSFEWMA NA    initialized
153 ZJYICBPX NA    initialized
164 AZBIUQMJ NA    initialized
17
18my_parent$run()
19my_parent$count()
20[1] 8
21
22my_parent$status()
23# A tibble: 4 × 3
24  name     value status
25  <chr>    <dbl> <chr> 
261 UWCITAHO     0 run   
272 KCSFEWMA     1 run   
283 ZJYICBPX     2 run   
294 AZBIUQMJ     4 run   

Running the code above creates a parent class instance called my_parent. Four steps are added to the parent class (step_add_one twice and step_double twice). At this point, the accumulator (my_parent$count()) is 0 and my_parent$status() shows all steps are initialized as no steps have been executed. After my_parent$run() is run and all steps executed, the accumulator is 8 (add 1, add 1, double, double) and my_parent$status() shows all steps are run.

The accumulator is a field in the parent and it is updated through the child classes.