Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested member functions & recursive self-binding #234

Open
mmuurr opened this issue Oct 20, 2021 · 6 comments
Open

Nested member functions & recursive self-binding #234

mmuurr opened this issue Oct 20, 2021 · 6 comments

Comments

@mmuurr
Copy link

mmuurr commented Oct 20, 2021

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!

@irudnyts
Copy link

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()

@dmurdoch
Copy link

dmurdoch commented Nov 14, 2023

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

@irudnyts
Copy link

irudnyts commented Nov 15, 2023

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?

@dmurdoch
Copy link

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.

@irudnyts
Copy link

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 🙂

@irudnyts
Copy link

Btw, if I'm not mistaken, Reference Classes have the same issue, no no-go there as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants