1

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__?

thothal
  • 16,690
  • 3
  • 36
  • 71
  • 1
    you can't use an `active` binding? – Stéphane Laurent Apr 20 '23 at 13:11
  • 1
    This would not resolve the scoping issue I guess? Problem boils down to: I define `calc` in the global environment, but I want it to access elements from **within** the `R6` class (whether `private`, `self` or `active` should not matter IMHO) – thothal Apr 20 '23 at 13:18

1 Answers1

1

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.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • 1
    Holy cow, so easy yet so elegant! Sometimes, one does not see the wood for the trees. Thanks. – thothal Apr 20 '23 at 13:41