6
votes

I am writing my first R package and I am trying to figure out what the best way to assign values to a slot in an S4 object, keeping in mind that end-users shouldn't have to fuss with the details of the S4 class structures being used. Which of the following is best?

  1. Accessing slot directly using object@MySlot <- value:

    I understand that this bad practice (e.g., this Q&A).

  2. Using slot(object, "MySlot") <- value:

    The R help says there isn't checking when getting the values, but there is checking when setting (assuming check hasn't been set to FALSE). This sounds reasonable to me, and strikes me as a nice way to do it because I don't have to code my own get/set methods as per below.

  3. Using custom methods with setReplaceMethod():

    How does this approach compare against the second option above? It's more work to produce the necessary get/set methods, but I can more explicitly be sure that the values being written to the slots are valid for that slot type.

    setGeneric("MySlot", function(object) {
        standardGeneric("MySlot")
    })
    
    setMethod("MySlot",
          signature = "MyClass",
          definition = function(object) {
              return(object@MySlot)
    })
    
    setGeneric("MySlot<-",
           function(object, value) {
               standardGeneric("MySlot<-")
    })
    
    setReplaceMethod("MySlot",
                 signature="MyClass",
                 function(object, value) {
                     object@MySlot<- value
                     validObject(object) # could add other checks
                     return(object)
    })
    
1

1 Answers

4
votes

By definition, "not having to fuss with the details of the S4 class structure" means end-users should not have to know about your slots. As such, any wrappers you write as in steps 2 and 3 are more for internal consistency checks. I think more important is to check for edge cases where you think your integrity checks would fail using unit tests.

As you have pointed out, #1 can be ruled out fairly easily, and should only be used in internal methods. Whether you encourage #2 or implement #3 depends on the contents of the variable and personal taste, but I would encourage the latter. For example, if you have a logical flag, you can use #2, but more descriptive would be enableFoo(). That being said, if you have to consider developer time (which in real life is almost always true) you should think briefly about the trade-off between relegating mutators to slot<- for members that probably won't be accessed frequently (e.g., by less than 1% of users), versus implementing custom mutators for everything as in #3.

Finally, since almost all three of R's OOP systems are essentially syntactic sugar and aren't really respected semantically by the language (where only S4 can claim some exception), it is easy to forget the fundamental ideas behind object-oriented programming as implemented in other languages: any code executing outside of your methods should not know about the object's members. You are providing an external interface to the world that is and should be a black box.