5
votes

I have a base class (let's call it "A") whose representation is common to many other classes.

Therefore I define other classes, such as "B", to contain this class.

I would like to set the prototype of these other classes (B) to include the default values for the slots inherited from A. I thought this would be natural:

setClass("A", representation(a="character"))
setClass("B", contains="A", prototype(a = "hello"))

But it produces the error:

Error in representation[!slots] : object of type 'S4' is not subsettable

Not sure why this happens. If I omit the prototype I can do:

setClass("B", contains="A")

and then hack my own generator function:

new_B <- function(...){ 
           obj <- new("B", ...)
           obj@a = "hello"
           obj
         }

and then create my object based on the prototype with new_B(), but that's terribly crude and ugly compared to using the generic generator new("B") and having my prototype...

2

2 Answers

5
votes

Supplementing my comment rather providing a new answer to the question, here's a solution where we still match arguments by position (because we specify additional representation for class B):

.A <- setClass("A", representation(a="character"))
.B <- setClass("B", representation(b="numeric"),
     prototype(a="hello"),
     contains="A")

.A() and .B() replace calls to new("A") and new("B"). At some level this is syntactic sugar, but can make object construction more transparent

## construct an object using B's prototype, like new("B", b=1:3)
> .B(b=1:3)
An object of class "B"
Slot "b":
[1] 1 2 3

Slot "a":
[1] "hello"

## construct an object using A's prototype, like new("B", new("A"), b=1:3)
> .B(.A(), b=1:3)
An object of class "B"
Slot "b":
[1] 1 2 3

Slot "a":
character(0)

(the second example uses the fact that unnamed arguments to new or B are used to initialize inherited classes).

It isn't so friendly for the user to have to use .A or .B directly, e.g., because the signature is just ... and so would be documented as 'see the definition of slots for class A'. This disrupts the separation of interface and implementation that is a strength of OOP. Also, one or the other behavior in the last code chunk (.B(.A(a=a), b=b) or .B(a=a, b=b)) might not be the intention. So instead provide a function that is exposed to the user, perhaps doing some initial data massage

A <- function(a=character(), ...) {
    ## nothing special, just a public constructor
    .A(a=a, ...)
}

B <- function(b, a="hello", ...) {
    a <- tolower(a)  ## no SHOUTing!
    .B(A(a=a), b=b)  ## A's public interface; no need for B to know A's details
}

The functions A and B define the interface, provide the user with hints about what acceptable arguments are without tying the constructor to the class definition, and perform preliminary data massaging. The latter can make initialize methods unnecessary, which is a good thing because these have a complicated contract (they're supposed to initialize and be copy constructors, and as we've seen above unnamed arguments are supposed to initialize base classes) that most people get wrong.

Mostly these are just my opinions.

2
votes

You just need to name the argument:

setClass("A", representation(a="character"))
setClass("B", contains="A", prototype=prototype(a="hello"))