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.