r-lib / R6

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

Passing an object declared in a file to the field of an R6 class defined in another file #276

Closed arnaudgallou closed 4 months ago

arnaudgallou commented 11 months ago

I'm developing a package using R6 classes. I'd like to pass the output of a function declared in a file as default value to the field of an R6 class defined in another file. E.g.:

# in file A
foo <- function(x = "blah", y = TRUE) {
  # do stuff
}

# in file B
Bar <- R6Class(
  "Bar",
  private = list(
    fld = foo()
  )
)

Doing this throws a could not find function "foo" error, which I guess is due to different environments between the class and the function. The only way to make it work is to declare foo (and all custom low-level functions it relies on) in the same file as Bar, which isn't always a good solution depending on the number of functions to declare at the top of file B.

In this simple example, there's no benefit of passing foo() directly to the field over populating the field within initialize().

In my actual case, Bar looks more like the following:

# case 1
Bar <- R6Class(
  "Bar",
  public = list(
    initialize = function(fld = NULL) {
      if (!is.null(fld)) {
        private$fld <- purrr::list_modify(private$fld, !!!fld)
      }
    }

    # ...
  ),
  private = list(
    fld = list(
      a = letters,
      c = NULL,
      b = foo()
    )
  )
)

Now the only way I can think of to make this work is to do something like:

# case 2
public = list(
  initialize = function(fld = NULL) {
    fld_default <- list(
      a = letters,
      b = NULL,
      c = foo()
    )
    if (!is.null(fld)) {
      private$fld <- purrr::list_modify(fld_default, !!!fld)
    } else {
      private$fld <- fld_default
    }
  }
),
private = list(fld = NULL)

or

# case 3
public = list(
  initialize = function(fld = NULL) {
    c_default <- foo()
    if (!is.null(fld)) {
      # add some logic to replace the default `c` value with the one provided by
      # the user, if supplied
      # populate `fld` field
    }
  }
),
private = list(
  fld = list(
    a = letters,
    b = NULL,
    c = NA
  )
)

Either way, it makes the code more complicated and not as clean as in case 1, especially if I have to do this for multiple fields.

Hence my question: is there a way to pass an object defined in a separate file to an R6 field?

arnaudgallou commented 10 months ago

I've just realised that the error actually depends on the file hierarchy in the package. If the function is stored in a script whose name comes before the file that contains the class, it works. Otherwise, it errors. So basically, the function or object has to be declared before the class (e.g. in an aaa file might be a good choice).

We can consider the issue solved but I'd highly recommend to have a word or two about hoisting in the documentation, especially since it's not the usual behaviour in R. I let you close the issue depending on whether you want to add that in the documentation or not.