r-lib / R6

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

setters with pass by reference semantics #78

Closed nverno closed 8 years ago

nverno commented 8 years ago

Would it make sense to have setter functions change fields by reference? Something like the following example,

## Make a setter function to modify a field without copying
library(inline)
setter <- cfunction(signature(field="list", index="integer",
                              value="character"),
                    body='SET_VECTOR_ELT(field, INTEGER(index)[0]-1, value); return R_NilValue;')

library(R6)
Example <- R6Class(
  "Example",
  public = list(
    example = NA,
    initialize = function(length) {
      if (!missing(length)) self$example <- vector("list", length)
    },
    set_example = function(index, value) {
      setter(self$example, as.integer(index), as.character(value))
      invisible()
    }
  )
)

example <- Example$new(length=10)
tracemem(example$example)
example$set_example(1, "B")
example$example[[1]]
wch commented 8 years ago

I don't think so - I believe that with R's reference counting, if there's only one reference to a list, then when you set an element in the list, it modifies the list directly, without making a copy.

Also, one danger of using the code you have there is if someone makes a copy of the list with x <- example$example, then modifying one will modify the other, which would be confusing.

nverno commented 8 years ago

I had thought it wouldn't make a copy of the list as well, but when experimenting with the above example using a regular setter function, ie, using simply

set_example = function(index, value) {
    self$example[[index]] <- value

and tracing the memory of example$example it seemed to be copying every time a value was set. I has some hazy idea that the reference counter could be incremented when values are passed to non-primitive functions. I agree about the confusing part in your danger warning -- I'll just close it unless there is interest.

wch commented 8 years ago

Interesting... I suspect that if you used R6Class(portable=FALSE), and then set it with example[[index]] <<- value, it won't make a copy. Using the form self$example[[index]] <- value may cause a copy to be made. If you test that out, can you report back what you find?

nverno commented 8 years ago

I tried with portable=FALSE, and using <<- in place of both <- in the initialize and set_example functions, but it still appears to me that a copy is being created (using tracemem). Ill just dump the whole thing here, in case I'm making a silly mistake,

Example2 <- R6Class(
  "Example2",
  public = list(
    example = NA,
    initialize = function(length) {
      if (!missing(length)) example <<- vector("list", length)
    },
    set_example = function(index, value) example[[index]] <<- value
  ),
  portable = FALSE
)

example <- Example2$new(length=10)
tracemem(example$example)
example$set_example(1, "B")
example$example[[1]]

Hopefully, I will get some more time over the next couple of days to look deeper into it, and maybe be able to report back a fuller description of what is going on. I love the package btw, and hope to be using it quite a bit.

wch commented 8 years ago

I think I have an idea of why this is happening. I've modified the code to move the tracemem() call inside of the initialize method. With this version of the code, it does modify example$example in place.

Example3 <- R6Class(
  "Example3",
  public = list(
    example = NA,
    initialize = function(length) {
      if (!missing(length)) example <<- vector("list", length)
      print(tracemem(example))
    },
    set_example = function(index, value) example[[index]] <<- value
  ),
  portable = FALSE
)

example <- Example3$new(length=10)
example$set_example(1, "B")
example$example[[1]]

# Check that the address hasn't changed
.Internal(inspect(example$example))

I believe it's because in Example2, there's the line tracemem(example$example) -- simply accessing example$example results in a copy of the object. I don't know that much about how the ref count mechanism works, but if it's involved here, I can only guess that it's not getting decremented after the tracemem(example$example).

Note also that when running test code like this, you may need to run the entire code as one block, instead of line-by-line, because R's automatic assignment of .Last.value can create another copy of an object.

wch commented 8 years ago

One more version... this version of the code is similar to Example2, but with assignment using self$example[[index]] <- value. The first time a value is set, it triggers tracemem, but subsequent settings do not do so, until a value from example$example is accessed.


Example4 <- R6Class(
  "Example4",
  public = list(
    example = NA,
    tracemem = tracemem,
    initialize = function(length) {
      if (!missing(length)) self$example <- vector("list", length)
    },
    set_example = function(index, value) self$example[[index]] <- value
  )
)

example <- Example4$new(length=10)
tracemem(example$example)
example$set_example(1, "B") # Triggers tracemem
example$set_example(1, "C") # Does NOT trigger tracemem
example$set_example(1, "D") # Does NOT trigger tracemem

# Access a value
example$example[[1]]
example$set_example(1, "E") # Triggers tracemem