r-lib / R6

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

Infinite recursion - super points to self? #202

Closed jeffwong-nflx closed 4 years ago

jeffwong-nflx commented 4 years ago

Minimal Reproducible Example with problem below:

ClassA <- R6Class("ClassA",
                  public = list(
                    data = NULL,
                    initialize = function(data) {
                      self$data = data
                    },

                    foo = function() {
                      print("foo in A")
                      bar()
                    },

                    bar = function() {
                      print("bar in A")
                    }
                  )
)

ClassB <- R6Class("ClassB",
                  inherit=ClassA,
                  public = list(
                    data = NULL,
                    initialize = function(data) {
                      self$data = data
                    },

                    foo = function() {
                      print("foo in B")
                      self$bar()
                    },

                    bar = function() {
                      print("bar in B")
                    }
                  )
)

ClassC <- R6Class("ClassC",
                  inherit=ClassB,
                  public = list(
                    data = NULL,
                    initialize = function(data) {
                      self$data = data
                    },

                    foo = function() {
                      print("foo in C")
                      self$bar()
                    },

                    bar = function() {
                      print("bar in C")
                      super$foo()
                    }
                  )
)

Each class here has methods foo and bar. When I invoke ClassB methods they behave as expected:

B = ClassB$new(data = data.frame(a = 1)); B$foo()

[1] "foo in B"
[1] "bar in B"

When I invoke ClassC methods, I expect to see

  1. Foo in C
  2. Bar in C
  3. (Jump to the super class which is ClassB)
  4. Foo in B
  5. Bar in B

But somehow this triggers an infinite recursion. Using the debugger I am seeing that step 3 jumps to classB, the super, then invokes foo, then invokes self$bar, but self is actually pointing to the original ClassC instance, not to the classB instance (super) we jumped to. This triggers an infinite recursion.

jeffwong-nflx commented 4 years ago

Another simpler example

ClassA <- R6Class("ClassA",
                  public = list(
                    data = NULL,
                    initialize = function(data) {
                      self$data = data
                    },

                    foo = function() {
                      print("foo in A")
                      bar()
                    },

                    bar = function() {
                      print("bar in A")
                    }
                  )
)

ClassB <- R6Class("ClassB",
                  inherit=ClassA,
                  public = list(
                    data = NULL,
                    initialize = function(data) {
                      self$data = data
                    },

                    foo = function() {
                      print("foo in B")
                      self$bar()
                    },

                    bar = function() {
                      print("bar in B")
                    }
                  )
)

ClassC <- R6Class("ClassC",
                  inherit=ClassB,
                  public = list(
                    data = NULL,
                    initialize = function(data) {
                      self$data = data
                    },

                    foo = function() {
                      print("foo in C")
                      super$foo()
                    },

                    bar = function() {
                      print("bar in C")
                      super$bar()
                    }
                  )
)

When I invoke ClassC methods I see

x = ClassC$new(data.frame(a = 1))
x$foo()
[1] "foo in C"
[1] "foo in B"
[1] **"bar in C"** an extra step here.
[1] "bar in B"

I feel this is a common pattern, where a subclass behaves very similarly to its parent class, but just adds an extra step (print in this case). But because I have overwrote bar in the subclass, it defers to ClassC's bar, which then invokes class B's bar. However, I was hoping to see foo in C, foo in B, and then bar in B.

wch commented 4 years ago

That's the way it's supposed to work. When you instantiate ClassC, it doesn't create three separate objects of ClassA, ClassB, and ClassC.

self always refers to the object that was instantiated (in your case, of ClassC).

This behavior is so that you can do things like this :

library(R6)
Operator <- R6Class("Operator",
  public = list(
    compute = function(a, b) {
      cat(paste0("The result is: ", self$op(a, b)))
    },
    op = function(a, b) {
      stop("Not defined")
    }
  )
)

Multiplier <- R6Class("Multiplier",
  inherit = Operator,
  public = list(
    op = function(a, b) {
      a * b
    }
  )
)

multiplier <- Multiplier$new()
multiplier$compute(4, 5)
#> The result is: 20