R6
R6 copied to clipboard
Nested member functions & recursive `self`-binding
I think this counts as a feature request, unless there's a simple pattern that exists to achieve the desired result that I haven't yet stumbled upon. I've found some instances where it'd be useful to 'nest' methods (mostly for clean naming, sometimes in codegen situations). In spirit, something like this, where f1
is not a proper member function, but instead a function within a member field:
Klass <- R6::R6Class("Klass",
public = list(
x = 0,
initialize = function(x) self$x <- x,
funlist = list(
f1 = function() self$x
)
)
)
obj <- Klass$new(1)
obj$funlist$f1() ## object 'self' not found
But the self
environment-binding during construction only sees funlist
as a list, not as a container of objects into which one might recurse and continue the self
environment-binding. (Same observation for any other named & bound environments, like private
.)
One way around this would be implement a special type of list
, something akin to (or exactly):
memberlist <- function(...) structure(list(...), class = "R6.memberlist")
Now, when examining a new instance's members to determine if each is a data field or method for self
-binding, one could recurse in the special case of a "R6.memberlist"
field:
...
funlist = R6::memberlist(
f1 = function() self$x
)
...
I realize this is pretty specialized behavior, but ES6 Classes have a few ways to achieve this, including arrow functions (since an arrowfun's this
is bound to the syntactically-defined container wrapping the function):
class Klass {
x = 0;
constructor(x) {
this.x = x
};
funlist = {
f1_traditional_nobind: function() {
return this.x;
},
f1_traditional_iifebind: (function() {
return this.x;
}).bind(this),
f1_arrow: () => this.x
};
};
obj = new Klass(1);
obj.funlist.f1_traditional_nobind(); // undefined because of unbound `this`
obj.funlist.f1_traditional_iifebind(); // works, though a bit ugly/obfuscated
obj.funlist.f1_arrow(); // works (though don't try this with ES5 'classes')
(There are other ways to use .bind(...)
to get there, too, but they start to feel pretty convoluted.)
Again, this feels like pretty low priority and perhaps helps a very small number of R6 users, but also would be a neat extension to allow a tad more flexibility to the self
/private
-binding process :-)
In any case, thanks for all the existing work on R6!
Oh, I faced exactly the same issue today. Is there any other workaround? Is there a way to access the enclosing "parent" environment from f1()
? Here is my tiny reprex:
library(R6)
Person <- R6Class("Person",
public = list(
name = NULL,
level1 = list(
level2 = list(
level3 = list(
method = function() {
print(self$name)
}
)
)
),
initialize = function(name) {
self$name = name
}
)
)
jay = Person$new(name = "Jay")
jay$level1$level2$level3$method()
A workaround is to declare those nested functions at the top level of public
or private
, then assign them into the nested location in initialize()
. Another one (that's a little fragile) is to create a top-level method that modifies a function to make it into a method, and call that in initialize()
. I say it's fragile, because it depends on the current implementation of methods in R6, but I'd guess that's unlikely to change.
Here's an example of the second approach. I've edited it from my original post, which wouldn't work with classes that had private methods. For the first approach, see your post on SO: https://stackoverflow.com/q/77477472 .
library(R6)
Person <- R6Class("Person",
public = list(
name = NULL,
level1 = list(
level2 = list(
level3 = list()
)
),
initialize = function(name) {
self$name = name
self$level1$level2$level3$method <-
private$makeMethod(function() {
print(self$name)
})
}
),
private = list(
makeMethod = function(f) {
env <- new.env(parent = environment(f))
myenv <- parent.env(environment())
for (name in ls(myenv))
env[[name]] <- get(name, envir = myenv)
environment(f) <- env
f
}
)
)
jay = Person$new(name = "Jay")
jay$level1$level2$level3$method()
#> [1] "Jay"
Created on 2023-11-14 with reprex v2.0.2
Thanks for your replies and approaches. I'm rather fond of the one on the SO. Do you think it's an ok-ish idea to develop a package using this approach?
I think the SO one is really likely to survive R6 and R updates.
A harder question is whether that's a good way to define a class. An alternative would be to replace the nested list with a single public method, called as jay$method("level1", "level2", "level3")
. I think that would have better documentation support in Roxygen. If you are thinking the list might change dynamically, it would make more sense to leave it as a list.
I prefer it because of the syntax. I'm mimicking a Python API package, which has nested attributes/methods. E.g., I'm trying to represent a Python method call object.level1.level2.method()
with object$level1$level2$method()
.
Of course, one workaround would be simply calling methods in R in the following manner obj$level1_level2_method()
. But from the aestetical point of view it is not really desirable 🙂
Btw, if I'm not mistaken, Reference Classes have the same issue, no no-go there as well.