2
votes

For an R package I am working on (which creates an S4 class), I would like to delay loading some data until the user actually asks for it (because it may or not be required and loading it takes a little while). This would require me to set a slot's value within its getter (aka accessor) method if it has not previously been loaded. But I cannot get the new value to "stick". Here is an MRE:

setClass("A", representation(value = "numeric"))

setGeneric("value<-", function(x, value) standardGeneric("value<-"))
setGeneric("value", function(x) standardGeneric("value"))

setMethod("value<-", signature("A", "numeric"),
          function(x, value) 
          {
            x@value = value
            x
          })    

setMethod("value", signature(x = "A"),
          function(x) 
          {
            if(!length(x@value))
              value(x) <- 20
            x@value
          })    

This produces the following result:

> a <- new("A")
> value(a)
[1] 20
> a
An object of class "A"
Slot "value":
numeric(0)

So the value() function returns the desired new value (20), but this value is not actually updated in the object. Doing x@value <- value instead of value(x) <- 20 in the getter did not succeed either.

It seems the problem is that I am not returning the updated object in my getter (the way my setter does), but I have something else to return in my getter (the value).

What is the Right Way To Do This™?

Thank you!

EDIT:

After futher study of S4 pass-by-value semantics, I have come to the conclusion that this is Simply Not Possible™. If a slot is updated, the object must be returned, and you can't return something else instead. Can someone confirm my conclusion is correct?

1
Look at the documentation for setRefClass or the R6 package. S4 does not have traditional OOP mutability.Alexis
Thanks @Alexis I have to use S4 for my current project, but those docs brought to mind a possible work around (see my answer below).Eric

1 Answers

0
votes

The comment by @Alexis mentioned R6 classes. While I have been asked to S4 classes for my current project (targeting BioConductor), the following sentence from the R6 ReferenceClasses docs caught my eye: 'Reference classes are implemented as S4 classes with a data part of type "environment".'

So if I really want mutable elements while using S4 classes, I could emulate R6 as follows:

setClass("A", representation(inMutable = "ANY", 
                             .env = "environment"))

A = function(inMutable = NULL, mutable = NULL) {
  x <- new("A", 
           inMutable = inMutable,
           .env = new.env())
  [email protected]$mutable  = mutable
  x@inMutable <- inMutable
  x
}

setGeneric("inMutable<-", function(x, value) standardGeneric("inMutable<-"))
setGeneric("inMutable", function(x) standardGeneric("inMutable"))
setGeneric("mutable<-", function(x, value) standardGeneric("mutable<-"))
setGeneric("mutable", function(x) standardGeneric("mutable"))

#setter
setMethod("inMutable<-", signature("A", "numeric"),
          function(x, value) 
          {
            x@inMutable <- value
            x
          })    

#getter
setMethod("inMutable", signature("A"),
          function(x) 
          {
            if (!length(x@inMutable))
              message("Hmmm, you haven't initialized a value for 'inMutable'.",
                      " I'm afraid I can't do anything about that now.")
            x@inMutable
          })    

#setter
setMethod("mutable<-", signature("A", "numeric"),
          function(x, value) 
          {
            [email protected]$mutable <- value
            x
          })    

#getter (mutable!)
setMethod("mutable", signature("A"),
          function(x) 
          {
            if (!length([email protected]$mutable)) {
              message("Ah. You haven't initialized a value for 'mutable'. ",
                      "Lucky for you I can initialize it for you now.")
              [email protected]$mutable = 12
            }
            [email protected]$mutable
          })    

Then I can do this:

> a <- A()
> mutable(a)
Ah. You haven't initialized a value for 'mutable'. 
Lucky for you I can initialize it for you now.
[1] 12
> mutable(a)
[1] 12
>
> inMutable(a)
Hmmm, you haven't initialized a value for 'inMutable'. 
I'm afraid I can't do anything about that now.
NULL
> inMutable(a) <- 18
> inMutable(a)
[1] 18
> 

Obviously, reference classes and R6 provide a much richer and more robust solution but in a pinch this seems like a viable S4 option. Of course, I haven't thoroughly tested this in the wild to see where it might break down.