7

This is a two part Question. I want to set the value of a prototype s4 object based on a different slots value and alternatively I want to implement this as a method.

I have an object I am trying to create. It has some slots. I would like to set a slots value based on the values put in from another slot. This is a simplified version of what I want to do.

i.e.,

setClass("Person",
    representation(name = "character", age = "numeric", doubleAge = "numeric"),
    prototype(name = "Bob", age = 5, doubleAge = 10) )

now I want to create an object but have the doubleAge value set itself based on the age slot.

p1 <- new("Person", name = "Alice", age = 6)

I see

An object of class "Person"                                                                                                      
Slot "name":                                                                                                                     
[1] "Alice"                                                                                                                  

Slot "age":                                                                                                                    
[1] 6                                                                                                                        

Slot "doubleAge":                                                                                                              
[1] 10       

but I want to see doubleAge be 12. In the prototype, I do not know how to change doubleAge = 10 to something like doubleAge = 2*age

So as a solution I tried to make a setting function init which sets the value after creation. This is the part 2 question.

setGeneric("init", "Person", function(object) {
    standardGeneric("init")
}
setMethod("init","Person", function(object) {
    object@doubleAge <- object@age*2
    object
}

if I print object@doubleAge in the method it returns 12 but it seems that the scope ends because it is 10 when it returns

Currently what works is very similar but it is not correct.

setGeneric("init<-", "Person", function(object) {
    standardGeneric("init<-")
}
setMethod("init<-","Person", function(object) {
    object@doubleAge <- object@age*2
    object
}

but then I have to call like init(p1) <- NULL which just seems weird. I know this example seems trivial but it is just a barebones example of a more complicated real world problem.

channon
  • 362
  • 3
  • 16

2 Answers2

4

It seems overriding the initialize method worked for me. For example

setClass("Person",
         representation(name = "character", age = "numeric", doubleAge = "numeric"),
         prototype(name = "Bob", age = 5, doubleAge = 10) )

setMethod("initialize", "Person", function(.Object, ...) {
  .Object <- callNextMethod()
  .Object@doubleAge <- .Object@age*2
  .Object
})

(p1 <- new("Person", name = "Alice", age = 6))
# An object of class "Person"
# Slot "name":
# [1] "Alice"
# Slot "age":
# [1] 6
# Slot "doubleAge":
# [1] 12

The callNextMethod() runs the "default" initializer to set up all the values that we are not messing with. Then we just change the values we want and return the updated object.

MrFlick
  • 195,160
  • 17
  • 277
  • 295
  • Is there a reason why you put `function(.Object, ...)` opposed to my implementation of `function(object, ...)`? – channon Feb 20 '19 at 21:34
  • I was copying an existing answer of mine that worked. The variable name shouldn't matter. – MrFlick Feb 20 '19 at 21:34
  • regarding the second part, what if I want to change the doubleAge variable for some reason later? for example I call my first init function I would need to do something like `p1 <- init(p1)` if I change the return statement which I guess is fine but I would rather just write `init(p1)` Is this a limitation of S4 objects copying? Time to move to reference objects? – channon Feb 20 '19 at 21:47
  • R is a functional language and the pattern in such languages is not to mutate state of objects, but to return new objects so that's what S3 and S4 methods try to stick to. You could consider using reference classes or just refactor your code with a more functional spirit like the rest of R rather than an a strict OOP style. – MrFlick Feb 20 '19 at 21:52
  • 1
    @mrFlick, this is actually one of the few times where the name of the variable *does* matter — R will substitute `.Object` if you do not use that yourself. – JDL Feb 21 '19 at 08:50
  • according to page 22 this `object` vs `.Object` is not standardized https://cran.r-project.org/doc/contrib/Genolini-S4tutorialV0-5en.pdf – channon Feb 21 '19 at 15:59
1

In addition to the above method for initialize, you may want to set a method for @<-. The reason for this is if you do something like

x <- new("person", age=5)

then you have a valid person with age=5 and doubleage=10. But what if you now do

x@age <- 6

? Now age is 6 but doubleage is still 10 so the object is not valid any more.

The R documentation claims that you can write a method for @<- that will fix this:

setMethod("@<-",signature(object="Person"),function(object,name,value){
  if(name=="age"){
    object@age <- x
    object@doubleAge <- x*2
  } else if(e2=="doubleAge"){
    object@doubleAge <- x
    object@age <- value/2
  } else slot(object,name) <- value
  object
})

however when you actually run the above, you get an error:

Error in setGeneric(f, where = where) : ‘@<-’ dispatches internally; methods can be defined, but the generic function is implicit, and cannot be changed.

This is a strange looking error, since we are not trying to redefine the generic. In fact we find, when we try

method.skeleton("@<-",signature(object="Person"))

that R reluctantly tells us

Error in genericForBasic(name) : methods may not be defined for primitive function ‘@<-’ in this version of R

So if you want to have the slots reliably be consistent, we will have to write our own getters and setters, along the lines of

setAge <- function(x,value){
  x@age <- value
  x@doubleAge <- value*2
  x
}
JDL
  • 1,496
  • 10
  • 18