Search code examples
rr6

Using a closure to generate an R6 binding


I'm using active bindings in an R6 class to check values before assignment to fields. I thought I could use a closure to generate the bindings as below, but this doesn't work.

The binding isn't evaluated in the way I expect (at all?) because the error shows the closure's name argument. What am I missing?

library(R6)
library(pryr)

# pass a field name to create its binding 
generate_binding <- function(name) {
  function(value) {
    if (!missing(value) && length(value) > 0) {
      private$name <- value
    } 
    private$name
  }
}

bind_x = generate_binding(x_)
# created as intended:
unenclose(bind_x)
# function (value) 
# {
#     if (!missing(value) && length(value) > 0) {
#         private$x_ <- value
#     }
#     private$x_
# }

MyClass <- R6::R6Class("MyClass",
  private = list(
    x_ = NULL
  ),
  active = list(
    x = bind_x
  ),
)

my_class_instance <- MyClass$new()
my_class_instance$x <- "foo"
# Error in private$name <- value :
#   cannot add bindings to a locked environment

Solution

  • I think you’re misunderstanding how closures work. unenclose is a red herring here (as it doesn’t actually show you what the closure looks like). The closure contains the statement private$name <- value — it does not contain the statement private$x_ <- value.

    The usual solution to this problem would be to rewrite the closure such that the unevaluated name argument is deparsed into its string representation, and then used to subset the private environment (private[[name]] <- value). However, this doesn’t work here since R6 active bindings strip closures of their enclosing environment.

    This is where unenclose comes in then:

    MyClass <- R6::R6Class("MyClass",
      private = list(
        x_ = NULL
      ),
      active = list(
        x = pryr::unenclose(bind_x)
      ),
    )