Search code examples
rrlangr6

Allow user of R6 class to provide function which access `self`


Goal

I want to tweak the following R6 class such that calc is not hard coded but can be provided by the user.

Hard Coded

library(R6)

A <- R6Class("A",
             public = list(
               run = function(x) {
                 private$calc(x)
               },
               result_codes = list(OK = 1, NOK = 2)
             ),
             private = list(
               calc = function(x) {
                 if (x >= 0) {
                   self$result_codes$OK
                 } else {
                   self$result_codes$NOK
                 }
               })
)
a <- A$new()
a$run(1)
# [1] 1

As a parameter

If I want the user to supply a custom calc function I could do the following:

B <- R6Class("B",
             public = list(
               initialize = function(calc) {
                 private$calc <- calc
               },
               run = function(x) {
                 private$calc(x)
               },
               result_codes = list(OK = 1, NOK = 2)
             ),
             private = list(
               calc = NULL
             )
)

Problem

I want that the user can use self$result_codes, but this does not work because the function is defined in the global environment where self is not known and not "within" the R6Class:

b <- B$new(function(x) {
  print(rlang::env_parent())
  if (x >= 0) {
    self$result_codes$OK
  } else {
    self$result_codes$NOK
  }
})

b$run(1)
# <environment: R_GlobalEnv>
#  Error in calc(...) : object 'self' not found

Thus, the user needs to provide calc like this:

b <- B$new(function(x) {
  me <- environment(rlang::caller_fn())
  if (x >= 0) {
    me$self$result_codes$OK
  } else {
    me$self$result_codes$NOK
  }
})

b$run(-1)
# [2]

which I find cumbersome. Thus, I wrapped calc in initialize, such that the user can simply type self$result_codes$OK without bothering about changing the environment:

B <- R6Class("B",
             public = list(
               initialize = function(calc) {
                 private$calc <- calc
                 environment(private$calc) <- self$.__enclos_env__
               },
               run = function(x) {
                 private$calc(x)
               },
               result_codes = list(OK = 1, NOK = 2)
             ),
             private = list(
               calc = NULL
             )
)

b <- B$new(function(x) {
  if (x >= 0) {
    self$result_codes$OK
  } else {
    self$result_codes$NOK
  }
})

b$run(-1)
# [1] 2

This feels extremely hackish, because I am using the internal environment .__enclos_env__ (it seems like a road to hell to use double underscored properties).

How would I solve this problem? Is the approach of setting the environment of private$calc the right direction? If so, how to avoid using .__enclos_env__?


Solution

  • A straightforward and fairly clean solution would be to pass self explicitly as a parameter to the calc callback:

    B <- R6Class(
        "B",
        public = list(
            initialize = function(calc) {
                private$calc <- calc
            },
            run = function(x) {
                private$calc(self, x)
            },
            result_codes = list(OK = 1, NOK = 2)
        ),
        private = list(
            calc = NULL
        )
    )
    
    b <- B$new(function(self, x) {
        if (x >= 0) {
            self$result_codes$OK
        } else {
            self$result_codes$NOK
        }
    })
    

    Fiddling with environments (similarly to how you have attempted it) can work, but is a lot more complex and makes the solution brittle.