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.
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
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.
I guess the downside is the lack of auto-complete help for internal functions (f$printx()
) and internal args f$printx(arg)
, seems unavoidable.
You can add autocompletion with this:
.DollarNames.wrapped <- function(x, pattern) {
ls(attr(x, "impl", exact = TRUE))
}
Errr, wow. That's amazing. Very cool.
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"
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.