r-lib / R6

Encapsulated object-oriented programming for R
https://R6.r-lib.org
Other
404 stars 56 forks source link

Decorators for methods #148

Closed gaborcsardi closed 5 years ago

gaborcsardi commented 6 years ago

I wonder if there is a way to use "decorators" (as in Python) for the methods. Decorators are basically functionals and they give a nice and concise way to transform functions. E.g. something like this would be cool:


library(R6)

deco <- function(fun) {
  fun
  function(...) {
    cat("Decorating\n")
    fun(...)
  }
}

A <- R6Class(
  "A",
  public = list(
    method = deco(function() {
      42
    })
  )
)

ins <- A$new()
ins$method()

But of course this fails

❯ ins$method()
Decorating
Error in fun(...) : could not find function "fun"

because the environment of the method is lost. I wonder if it was possible to keep it, somewhere before the object's environment. Maybe not, just wondering, really.

wch commented 6 years ago

Here's a slightly modified version:

library(R6)

x <- 1

deco <- function(fun, y) {
  force(fun)
  force(y)
  function(...) {
    cat("Decorated\n")
    fun(...) + x + y
  }
}

A <- R6Class(
  "A",
  public = list(
    method = deco(function() { self$z }, 10),
    z = 100
  )
)

a <- A$new()
a$method()

These are the three functions involved:

fun is the function whose environment should change on instantiation or cloning. In order to do that, we would need to reach into method's enclosing environment and essentially do:

e <- environment(method)
environment(e$fun) <- private$.__enclos_env__

To make this work, we would need to tell R6Class how to find fun, given method. The challenge would be to come up with a decent API for this.

Another thing that works right now is to call self$method <- deco(self$method) in initialize:

library(R6)

x <- 1

deco <- function(fun, y) {
  force(fun)
  force(y)
  function(...) {
    cat("Decorated\n")
    fun(...) + x + y
  }
}

A <- R6Class(
  "A",
  public = list(
    initialize = function() {
      unlockBinding("method", self)
      self$method <- deco(self$method, 10)
    },
    method = function() { self$z },
    z = 100
  )
)

a <- A$new()
a$method()
#> Decorated
#> [1] 111

The unlockBinding is not great, but that could be made into a parameter for R6Class. lock_objects=F currently allows new members to be added, but don't allow existing ones to be changed. Maybe it could do that.

The other problem is that cloning won't work properly, but maybe that could be worked around somehow.

gaborcsardi commented 6 years ago

Yeah, I ended up with a similar approach, and calling a decorate() function from init: https://github.com/r-lib/ps/blob/764dc78c03691d5692f28c874e87bd31a924dd85/R/decorators.R

I think in general it would be useful to allow the methods to be a closure, I am not sure what the best syntax would be.

wch commented 5 years ago

If there were some standard for decorators to add a "decorator" class (and possibly some attributes), that could make this request feasible. That could also help solve the issue that @hadley mentioned elsewhere: that decorators obscure arguments to the function, and that decorated functions don't print nicely.

gaborcsardi commented 5 years ago

decorators obscure arguments to the function, and that decorated functions don't print nicely.

These can be fixed, though, but yeah, without decorators in general, I'll just close this.