memoise icon indicating copy to clipboard operation
memoise copied to clipboard

Create compatibility with R6 methods?

Open yogat3ch opened this issue 3 years ago • 8 comments

It looks like memoise is not compatible with R6 methods. What's the feasibility of creating compatibility with R6?

ther6 <- R6::R6Class(
  "test",
  lock_objects = FALSE,
  public = list(
    fun = memoise::memoise(function() {
      Sys.sleep(5)
      "foo"
    })
    ),
  private = list(
    baz = 1
  )
)

test <- ther6$new()

v <- test$fun()

yogat3ch avatar Nov 03 '21 17:11 yogat3ch

I think the way to make this work is to assign it in the constructor:

ther6 <- R6::R6Class(
  "ther6",
  public = list(
    initialize = function() {
      self$fun <- memoise::memoise(function() {
        message("Running fun()")
        "foo"
      })
    },
    fun = NULL
  )
)

test <- ther6$new()

test$fun()
#> Running fun()
#> [1] "foo"

test$fun()
#> [1] "foo"

Note that fun would be memoized once for each instance of ther6. If you wanted to share the memoized function, you'd have to define it outside of the class, and assign it in the initialize method:

fun_shared <- memoise::memoise(function() {
  message("Running fun()")
  "foo"
})

ther6 <- R6::R6Class(
  "ther6",
  public = list(
    initialize = function() {
      self$fun <- fun_shared
    },
    fun = NULL
  )
)

test <- ther6$new()
test2 <- ther6$new()

test$fun()
#> Running fun()
#> [1] "foo"

test2$fun()
#> [1] "foo"

This obviously won't work well if you want to make use of self in the memoized function.

Another route you could take is to do self$fun <- memoise(....) in the initialize method, as in the first example, but share the caching object across instances.

wch avatar Nov 05 '21 03:11 wch

I'm definitely using the self object in all the memoised functions. I'll have to try this last method

Another route you could take is to do self$fun <- memoise(....) in the initialize method, as in the first example, but share the caching object across instances.

Thanks for the input!

yogat3ch avatar Jan 06 '22 16:01 yogat3ch

@wch Could you please give a concrete implementation for

Another route you could take is to do self$fun <- memoise(....) in the initialize method, as in the first example, but share the caching object across instances.

Thank you!

teucer avatar Feb 01 '22 20:02 teucer

@teucer here you go:

cm <- cachem::cache_mem()

ther6 <- R6::R6Class(
  "ther6",
  public = list(
    initialize = function() {
      self$fun <- memoise::memoise(function() {
        message("Running fun()")
        "foo"
      }, cache = cm)
    },
    fun = NULL
  )
)

test <- ther6$new()

test$fun()
#> Running fun()
#> [1] "foo"

test$fun()
#> [1] "foo"

wch avatar Feb 02 '22 03:02 wch

@wch this seems to be similar to first version. How is it different?

I think it would advantageous to be able to memoise only once for the whole class (like python class methods).

I tried to initialize fun directly instead of NULL, but it does not work.

teucer avatar Feb 02 '22 10:02 teucer

@teucer Sorry, I copied and pasted the wrong code. I've fixed it in my comment above.

wch avatar Feb 02 '22 14:02 wch

@wch I'm curious if this approach is frowned-upon (when compared to the 'assign-to-NULL' approach shown above):

Klass <- R6::R6Class(
  public = list(
    echo = function(x) {
      print("echoing")
      x
    },
    initialize = function() {
      base::unlockBinding("echo", self)
      self$echo <- memoise::memoise(self$echo)
      base::lockBinding("echo", self)
    }
  )
)

obj <- Klass$new()

obj$echo(1)
# echoing
# [1]

obj$echo(1)
# [1]

The difference here is echo is defined as a function in the class generator object, as opposed to being originally-defined as a NULL field then assigned as a method.

Does this have any practical difference in the final objects? I've done some light testing that suggests, "no", but I might be missing a nuanced detail re: redefining methods vs converting a field to a method. (For example, any concerns with clone()?)

mmuurr avatar May 09 '22 18:05 mmuurr

@mmuurr That looks like OK to me. One thing to keep in mind is that, if the function actually uses self, as in self$n, it may be surprising for someone that it won't re-execute when self$n changes. So echo in this case looks like a method, but if you actually do method-y things with it (and access self), you can get into trouble.

If you clone the object, the cloned object's echo function will be the same object as the original's echo, and so self will refer to the wrong thing. Here's an example:

Klass <- R6::R6Class(
  public = list(
    x = 1,
    getx = function() {
      print("echoing")
      self$x
    },
    initialize = function() {
      base::unlockBinding("getx", self)
      self$getx <- memoise::memoise(self$getx)
      base::lockBinding("getx", self)
    }
  )
)

obj <- Klass$new()

obj$getx()
#> [1] "echoing"
#> [1] 1

obj2 <- obj$clone()
obj2$x <- 5
obj2$getx()
#> [1] 1

There may also be problems with inheritance, but I don't know for sure offhand.

wch avatar May 11 '22 15:05 wch