r-lib / R6

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

Issues with explicit arguments in $new() #193

Closed wch closed 4 years ago

wch commented 4 years ago

PR #103, which evolved into #191 (by @krlmlr and @wch) did not evaluate default arguments in the correct scope. For example, consider the following:

init_x <- 1
A <- R6Class("A", public = list(
  initialize = function(x = init_x, y = self$init_y) {
    self$x <- x
    self$y <- y
  },
  x = NULL,
  y = NULL,
  init_y = 2
))

a <- A$new()
a$x # should be 1
a$y # should be 2

The defaults for x and y should be evaluated inside the initialize() functions execution environment, but the change in those PRs did not do that, and calling A$new() would throw an error, because the it tried to evaluate those expressions in the calling environment.

I've made a fix for that in abd8171, and also tried to pass along the "missingness" of arguments, but there's still a few more issues.

First, the positions of arguments don't always work correctly if there are empty arguments (just a comma with nothing before it). Here's an example:

  A <- R6Class("A", public = list(
    initialize = function(x = 1, y = 2, ..., z) {
      self$x <- x
      self$y <- y
      self$dots <- list(...)
      self$z <- if (missing(z)) 3 else z
    },
    x = NULL,
    y = NULL,
    z = NULL,
    dots = list()
  ))

We would expect it to behave this way:

a <- A$new(,20,100)
# All of these are TRUE
identical(a$x, 1)
identical(a$y, 20)
identical(a$dots, list(100))

However, in abd8171, it behaves like this:

a <- A$new(,20,100)
# All of these are TRUE
identical(a$x, 100)
identical(a$y, 20)
identical(a$dots, list())

That is, the third argument, 100, gets treated as x.

The next problem involves nonstandard evaluation. In the CRAN version of R6, here's what happens if you call substitute() inside of the initializer -- it can capture the expression from the call to $new():

library(R6)
A <- R6Class("A", public = list(
  initialize = function(expr) {
    self$expr <- substitute(expr)
  },
  expr = NULL
))

a <- A$new(x+y+z)
identical(a$expr, quote(x+y+z)) # TRUE

This works because $new() simply takes ... as the only argument, and when $initialize() calls substitute(), the expr gets resolved back to the call to $new() -- that's a special property of using ....

In the dev version, here's what happens:

a <- A$new(x+y+z)
#> Error in A$new(x + y + z) : object 'x' not found

The substitute() doesn't know to go two levels back. I have a feeling this problem can't be solved, and so we'll have to revert the changes.