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

R6 equivalent to the python __call__ dunder method #220

Closed
mattwarkentin opened this issue Nov 29, 2020 · 7 comments
Closed

R6 equivalent to the python __call__ dunder method #220

mattwarkentin opened this issue Nov 29, 2020 · 7 comments

Comments

@mattwarkentin
Copy link

mattwarkentin commented Nov 29, 2020

Hi @wch,

Is there any R6 equivalent to the __call__ dunder method in python which essentially makes the class instance callable like a function? Is this a possible feature that could be added? Would there be any interest in this feature?

Something like:

foo <- R6::R6Class(
  "foo",
  public = list(
    .__call__ = function(args) {...}
  )
)

bar <- foo$new()
bar(...)

Pretty much all other dunder methods can be implemented in R by writing S3 methods for existing generics, but this one seems like it would need to be handled internally by R6, if it's possible at all.

@wch
Copy link
Member

wch commented Nov 29, 2020

I'm pretty sure that in R, using () will only work if the object is a function. What you can do is return the function, and add the R6 object as an attribute to it.

library(R6)

Foo <- R6Class("Foo",
  public = list(
    call = function() {
      self$x
    },
    x = 10,
    printx = function() {
      cat("The value of x is", self$x)
    }
  )
)

foo <- function() {
  obj <- Foo$new()

  # Return the obj$call method, and attach the full object as an attribute named
  # 'impl'. Also add the class "wrapped" to it so that we can define `$` and 
  # `$<-` methods for it.
  structure(
    obj$call,
    impl = obj,
    class = "wrapped"
  )
}

# The $ and $<- methods essentially pass through to the "impl" object attached
# to the function.
`$.wrapped` <- function(x, name) {
  obj <- attr(x, "impl", exact = TRUE)
  obj[[name]]
}

`$<-.wrapped` <- function(x, name, value) {
  obj <- attr(x, "impl", exact = TRUE)
  obj[[name]] <- value
  x
}



f <- foo()
f()
#> [1] 10
f$printx()
#> The value of x is 10
f$x
#> [1] 10

f$x <- 20
f()
#> [1] 20
f$printx()
#> The value of x is 20

@mattwarkentin
Copy link
Author

The dunder call method felt very un-R-like, so I kinda figured it wasn't possible. This is an interesting workaround though. Very clever. Thanks for the response.

@mattwarkentin
Copy link
Author

mattwarkentin commented Nov 30, 2020

I guess the downside is the lack of auto-complete help for internal functions (f$printx()) and internal args f$printx(arg), seems unavoidable.

@wch
Copy link
Member

wch commented Nov 30, 2020

You can add autocompletion with this:

.DollarNames.wrapped <- function(x, pattern) {
  ls(attr(x, "impl", exact = TRUE))
}

@mattwarkentin
Copy link
Author

Errr, wow. That's amazing. Very cool.

@mattwarkentin
Copy link
Author

Hi @wch,

I've got another weird question that you may be able to solve. But this one is especially odd.

Similar to the above questions related to having an object that looks and feels like a normal object, but has some encapsulated methods. I was wondering if it is possible to have the global object update when the value it is based on is updated in the R6 instance? This is perhaps a bit confusing so here is an example.

x is the object we want users to work with - It is just an integer. But through the magic you shared, you pack an R6 object as an attribute so some internal methods are available (e.g. x$update()). But when you make an update with x$update(20), it only updates self$x and not x. Is it possible to propagate the change to the global definition of x, maybe with super-assignment?

library(R6)

Foo <- R6Class(
  "Foo",
  public = list(
    x = 10,
    update = function(value) {
      self$x <- value
    }
  )
)

foo <- function() {
  obj <- Foo$new()
  structure(
    obj$x,
    impl = obj,
    class = "wrapped"
  )
}

`$.wrapped` <- function(x, name) {
  obj <- attr(x, "impl", exact = TRUE)
  obj[[name]]
}

`$<-.wrapped` <- function(x, name, value) {
  obj <- attr(x, "impl", exact = TRUE)
  obj[[name]] <- value
  obj$x
}

.DollarNames.wrapped <- function(x, pattern) {
  ls(attr(x, "impl", exact = TRUE))
}


x <- foo()
x
#> [1] 10
#> attr(,"impl")
#> <Foo>
#>   Public:
#>     clone: function (deep = FALSE) 
#>     update: function (value) 
#>     x: 10
#> attr(,"class")
#> [1] "wrapped"
x$update(20)
x
#> [1] 10
#> attr(,"impl")
#> <Foo>
#>   Public:
#>     clone: function (deep = FALSE) 
#>     update: function (value) 
#>     x: 20
#> attr(,"class")
#> [1] "wrapped"

@mattwarkentin
Copy link
Author

I guess it can be summarized as modification-in-place, but in a situation where we want the object to look and feel like a familiar and common R object, but have these embedded methods that act on the object.

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

2 participants