r-lib / R6

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

Implement deep cloning #65

Closed wch closed 9 years ago

wch commented 9 years ago

This is an additional component for #27. It implements deep cloning.

If x$clone(deep=TRUE) is called, it will clone the object, and also make copies of any fields that are R6 objects. For example:

Simple <- R6Class("Simple",
  public = list(
    x = NULL,
    initialize = function(val) self$x <- val
  )
)

Cloner <- R6Class("Cloner",
  public = list(
    s = NULL,
    y = 1,
    initialize = function() self$s <- Simple$new(1)
  )
)

a <- Cloner$new()
b <- a$clone()
c <- a$clone(deep = TRUE)

# Modify a
a$s$x <- 2
a$y <- 2

# b is a shallow clone, so b$s is the same as a$s because they are R6 objects.
b$s$x
#> [1] 2
# But a$y and b$y are different, because y is just a value.
b$y
#> [1] 1

# c is a deep clone, so c$s is not the same as a$s.
c$s$x
#> [1] 1
c$y
#> [1] 1

If this behavior isn't exactly what the user wants, it's also possible to use a custom private$deep_clone function. This idea was inspired by the method used in @thomasp85's pull request #57.

From the help: If you want different deep copying behavior, you can supply your own private method called deep_clone. This method will be called for each field in the object, with two arguments: name, which is the name of the field, and value, which is the value. Whatever the method returns will be used as the value for the field in the new clone object. You can write a deep_clone method that makes copies of specific fields, whether they are environments, R6 objects, or reference class objects.

An example of making a deep clone of an object where the fields include an environment and R6 objects. In this example, some, but not all, of the R6 fields are copied.

Simple <- R6Class("Simple",
  public = list(
    x = NULL,
    initialize = function(val) self$x <- val
  )
)

# Deep cloning with custom function
CustomCloner <- R6Class("CustomCloner",
  public = list(
    e = NULL,
    s1 = NULL,
    s2 = NULL,
    s3 = NULL,
    initialize = function() {
      self$e <- new.env()
      self$e$x <- 1
      self$s1 <- Simple$new(1)
      self$s2 <- Simple$new(1)
      self$s3 <- Simple$new(1)
    }
  ),
  private = list(
    # With x$clone(deep=TRUE) is called, the deep_clone gets invoked once for
    # each field, with the name and value.
    deep_clone = function(name, value) {
      if (name == "e") {
        # e1 is an environment, so use this quick way of copying
        list2env(as.list.environment(value, all.names = TRUE))

      } else if (name %in% c("s1", "s2")) {
        # s1 and s2 are R6 objects which we can clone
        value$clone()

      } else {
        # For everything else, just return it. This results in a shallow
        # copy of s3.
        value
      }
    }
  )
)

a <- CustomCloner$new()
b <- a$clone(deep = TRUE)

# Change some values in a's fields
a$e$x <- 2
a$s1$x <- 3
a$s2$x <- 4
a$s3$x <- 5

# b has copies of e, s1, and s2, but shares the same s3
b$e$x
#> [1] 1
b$s1$x
#> [1] 1
b$s2$x
#> [1] 1
b$s3$x
#> [1] 5
thomasp85 commented 9 years ago

I like your approach with providing a function that evaluate each field rather than my approach with returning a list of lists with new values - it seems cleaner code-wise. Good job