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?
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())
}
)