jcubic
jcubic

Reputation: 66560

How to add functions in a loop to R6Class in R

I want to write wrapper over R6Class but it don't work, I've tried multiple things after I've found this question dynamically add function to r6 class instance

So I've tried this, they both don't work:

get <- function(x = list()) {
  class <- R6::R6Class(classname = "class")
  for (name in names(x)) {
    class$set("public", name, function() name)
  }
  class
}

x <- get(x = list(a = 10, b = 20))$new()
x$a()
# b
x$b()
# b

this is because of the looping with closures the for don't create new scope. So I've tried this:

get <- function(x = list()) {
  class <- R6::R6Class(classname = "class")
  lapply(names(x), function(name) {
    print(name)
    class$set("public", name, function() name)
  })
  class
}

x <- get(x = list(a = 10, b = 10))$new()
x$a()

this will throw error that name is not defined, because this behavior of R6Class that everything is is in eval substitute, so there is no way to make new function that take scope/environment from where it was called. Or is there a way?

My real issue is that I want to create function wrapper and I want to call:

fn <- function() {
    x <- 10
    y <- myFunction(public = list(
       foo = function(y) {
          x + y
       }
    })
    z <- y$new()
    z$foo(10)
    ## I want 20 as result
}

Is there a way to create myFunction function that will create R6Class? The reason why I want this is because I have component system based on R6Class and want to remove some boilerplate that need to be added to each class so it's easier to use. I don't want to create new class system, I would like to use R6 classes.

Upvotes: 2

Views: 320

Answers (1)

jcubic
jcubic

Reputation: 66560

I've asked on GitHub after adding this question and they given the answer very quickly. Here is re-post of the answer:

get <- function(x = list()) {
  class <- R6::R6Class(classname = "class")
  lapply(names(x), function(name) {
    fn <- eval(substitute(function() subst_name, list(subst_name = name)))
    class$set("public", name, fn)
  })
  class
}
x <- get(x = list(a = 10, b = 20))$new()
x$a()

and if you want better name when you print x$a you can clear name ref using:

attr(fn, "srcref") <- NULL

EDIT:

and here is example when values added to class are functions (this is my improved code):

constructor <- function(public = list(), private = list()) {
  class <- R6::R6Class(classname = "class")
  lapply(names(public), function(name) {
    if (is.function(public[[name]])) {
      env <- environment(public[[name]])
      env$self <- public
      env$private <- private
      fn <- eval(substitute(function(...) fn(...), list(fn = public[[name]])))
      class$set("public", name, fn)
    } else {
      class$set("public", name, public[[name]])
    }
  })
  class
}
test <- function() {
  a <- 10
  class <- constructor(
     public = list(
         a = function() { a + self$b },
         b = 20
     )
  )
  x <- class$new()
  x$a()
}

test()

and if you want to have access to super you need to use this code:

EDIT2:

component <- function(public = NULL,
                      private = NULL,
                      static = NULL,
                      ...) {
  class <- R6::R6Class(...)
  r6.class.add(class, public)
  r6.class.add(class, private)
  class$extend <- make.extend(class)
  class
}

#' helper function for adding properties to R6Class
r6.class.add <- function(class, seq) {
  prop.name <- as.character(substitute(seq)) # so we don't need to write name as string
  lapply(names(seq), function(name) {
    if (is.function(seq[[name]])) {
      ## the only way to have scope from when function was create with self and private
      ## eval substitute simply circument R6 encapsulation and use scope from where function
      ## was created (closure) and env.fn patch the env of inner function so it get self
      ## and private as magic names - this is done so component function the same as
      ## when R6Class is created inline - so component is referencial transparent and can
      ## be replaced with R6Class
      fn <- eval(substitute(function(...) {
        ## patch function env
        fn <- fn.expr # fn.expr will be inline function expression
        parent <- parent.env(environment())
        ## we don't overwrite function scope so you can nest one constructor
        ## in another constructor
        env <- new.env(parent = environment(fn))
        env$self <- parent$self
        env$super <- parent$super
        env$private <- parent$private
        environment(fn) <- env
        fn(...)
      }, list(fn.expr = seq[[name]], name = name)))
      class$set(prop.name, name, fn)
    } else {
      class$set(prop.name, name, seq[[name]])
    }
  })
}

instead of env$self <- parent$self you can also use get("self", parent) (it will search the environment chain for the variable).

Upvotes: 2

Related Questions