Search code examples
rr-s4

Dynamically naming objects to be updated


I have defined an S4 Class with a slot that is a list. I have written a method (based on Genolini's introduction to S4 - section 10.2) to append a new entry to that list:

setClass("MyClass",
         slots = c(entries = "list")
)
a1 <- new("MyClass", entries = list(1))

setGeneric(name="MyAppend",
           def=function(.Object, newEntry)
           {
             standardGeneric("MyAppend")
           }
)


setMethod(f = "MyAppend",
          signature = "MyClass",
          definition = function(.Object, newEntry){
            nameObject <- deparse(substitute(.Object)) 
            newlist <- .Object@entries  
            n  <- newlist %>% length 
            newlist[[n + 1]] <- newEntry  
            .Object@entries  <- newlist
            assign(nameObject, .Object, envir = parent.frame())
            return(invisible)
          }
)

If I then run

MyAppend(a1, 2)
a1

I get

R>a1
An object of class "MyClass"
Slot "entries":
[[1]]
[1] 1

[[2]]
[1] 2

which is just as it should be.

But in my application I will be generating the names of the objects to be updated dynamically:

ObjectName <- paste0("a", 1)

then I can turn that name into the object itself with

Object <- ObjectName %>% sym %>% eval

and then str(Object) returns

Formal class 'MyClass' [package ".GlobalEnv"] with 1 slot   
..@ entries:List of 3
   .. ..$ : num 1
   .. ..$ : num 2  

which, again, is just as it should be.

But when I run

MyAppend(Object, 3)
Object
a1

I get the following that shows that while Object has been updated a1 has not been.

R>Object
An object of class "MyClass"
Slot "entries":
[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] 3


R>
R>a1
An object of class "MyClass"
Slot "entries":
[[1]]
[1] 1

[[2]]
[1] 2

What am I doing wrong, please?


Solution

  • The problem is that this line:

    Object <- ObjectName %>% sym %>% eval
    

    Doesn't do what you think it does. The right hand side evaluates to the object a1, so it is no different to doing

    Object <- a1
    

    But this creates a copy of a1, it does not create a reference or a pointer or a synonym for a1.

    It is possible to create a reference (of sorts) by passing the unevaluated name of the object you wish to append to your generic method. If you leave out the eval part of ObjectName %>% sym %>% eval then Object gets assigned the name a1, which can be passed as a reference to the object a1.

    However, this leaves you with a new problem: MyAppend doesn't know what to do with an object of class name. You therefore need to write a suitable method for dealing with names:

    setMethod(f = "MyAppend",
              signature = "name",
              definition = function(.Object, newEntry){
                 stopifnot(class(eval(.Object)) == "MyClass")
                 objname <- as.character(.Object)
                 .Object <- eval(.Object)
                 .Object@entries <- append(.Object@entries, newEntry)
                 assign(as.character(objname), .Object, envir = parent.frame())
              }
    )
    

    Now let's see how this would work:

    a1 <- new("MyClass", entries = list(1))
    a1
    #> An object of class "MyClass"
    #> Slot "entries":
    #> [[1]]
    #> [1] 1
    
    MyAppend(a1, 2)
    a1
    #> An object of class "MyClass"
    #> Slot "entries":
    #> [[1]]
    #> [1] 1
    #> 
    #> [[2]]
    #> [1] 2
    
    Object <- paste0("a", 1) %>% sym()
    
    MyAppend(Object, 3)
    a1
    #> An object of class "MyClass"
    #> Slot "entries":
    #> [[1]]
    #> [1] 1
    #> 
    #> [[2]]
    #> [1] 2
    #> 
    #> [[3]]
    #> [1] 3
    

    I think this was what you intended. You may wish to consider having a method that dispatches character strings to make this workflow easier (you would use get inside the method to retrieve the object from the name passed as a character string)


    Note that I altered your own function as well; you shouldn't do return(invisible), since this returns the body of the built-in function invisible. Just leave the return statement out altogether. You can also make use of the built-in function append, to make your method for MyClass a bit simpler:

     setMethod(f = "MyAppend",
              signature = "MyClass",
              definition = function(.Object, newEntry){
               nameObject <- deparse(substitute(.Object)) 
               .Object@entries <- append(.Object@entries, newEntry)
               assign(nameObject, .Object, envir = parent.frame())
              }
    )