r-lib / R6

Encapsulated object-oriented programming for R
https://R6.r-lib.org
Other
405 stars 56 forks source link

Strange bug with capturing namespaces #94

Closed dselivanov closed 7 years ago

dselivanov commented 7 years ago

First of all many thanks for the huge amount of work done by R6 developers and contributors. I use R6 v2.1.3. Recently I discovered a strange behaviour of R6 classes (looks like a bug) - when instance of the class is cloned, it can't correctly capture namespace/environment of the member function. Description above can looks unclear, so it will be simpler to consider following example (my use case a little bit more complex - I use R6 to create iterators which can apply some function to the chunk after $next() call):

# create trait-like class with a placeholder for function
fun_caller = R6::R6Class(
  public = list(
    initialize = function(f_) self$f = f_,
    f = NULL,
    call_f = function(x) self$f(x)
  )
)
library(tokenizers)
# from this library we will need `tokenize_words()` function. 
# It internally use some utility function `check_input()` which is not exposed!
fun_caller_instance = fun_caller$new(tokenize_words)

fun = function(caller, txt) {
  # clone object because I don't want to modify input to be consistent with usual R behaviour
  internal_caller = caller$clone() # fails for both deep = TRUE or FALSE
  internal_caller$call_f(txt)
}
fun(fun_caller_instance, "abc def")

Error in self$f(x) : could not find function "check_input"

  1. self$f(x)
  2. internal_caller$call_f(txt)
  3. fun(fun_caller_instance, "abc def")

check_input - is a internal function in tokenizers package and it is not exported!

However this works fine:

# note that there is no clone() call
fun2 = function(caller, txt) {
  caller$call_f(txt)
}
fun2(fun_caller_instance, "abc def")

[[1]] [1] "abc" "def"

And this also works:

tokenize_words2 = function(x, ...) tokenize_words(x, ...)
fun_caller_instance = fun_caller$new(tokenize_words2)

fun3 = function(caller, txt) {
  internal_caller = caller$clone() # fails for both deep = TRUE or FALSE
  internal_caller$call_f(txt)
}
fun3(fun_caller_instance, "abc def")

[[1]] [1] "abc" "def"

dselivanov commented 7 years ago

@wch If this involves deep digging, can you suggest some workarounds?

wch commented 7 years ago

The reason it happens is because the clone method makes this assumption: any members of the R6 object that are functions are also methods on the object, and it will reassign the environment of the function after it copies it to the new object. I'm not sure whether this is behavior that I'd want to keep or not -- there are some pros and cons to it.

One thing you could do for now is simply wrap the function in a list, so that clone won't think it's a method and reassign the environment. So you'd have something like this:

fun_caller = R6::R6Class(
  public = list(
    initialize = function(f_) self$f = list(f_),
    f = list(),
    call_f = function(x) self$f[[1]](x)
  )
)