Question

If I have an S4 class such as:

setClass("MyClass",
     representation(
       data="data.frame",
       name="character"))

and instantiate it (say to obj),

obj <- new('MyClass', data=data.frame(1:3), name='An S4 class')

I will have the following representation:

An object of class "MyClass"
Slot "data":
  X1.3
1    1
2    2
3    3

Slot "name":
[1] "An S4 class"

So far so good.

However, if I try to change the "class" using:

class(obj) <- "animal"

I now get

An object of class "animal"
<S4 Type Object>
attr(,"data")
  X1.3
1    1
2    2
3    3
attr(,"name")
[1] "An S4 class"

And if I try to check whether it is still an S4 class, it will return true:

>isS4(obj)
[1] TRUE

What is happening exactly? Why did the "slots" changed to attributes? Is this really still an S4 class?

UPDATE:

Thank you for the comprehensive answers. Just to clarify, I wasn't expecting this to work or to be used in any real scenario. I was just wanted to understand better the mechanism behind this behaviour. Also, it's hard to pick a "best" answer (they're all excellent) but, within the spirit of SO, I must pick one.

Was it helpful?

Solution

S4 implements slots as attributes. This is usually hidden from the user, but is easily seen

> attributes(setClass("MyClass", representation(x="integer"))())
$x
integer(0)

$class
[1] "MyClass"
attr(,"package")
[1] ".GlobalEnv"

In a little more gory detail, we have

> .Internal(inspect(setClass("MyClass", representation(x="integer"))()))
@1fe4dfd8 25 S4SXP g0c0 [OBJ,NAM(2),S4,gp=0x10,ATT] 
ATTRIB:
  @1fe4dfa0 02 LISTSXP g0c0 [] 
    TAG: @23c8978 01 SYMSXP g0c0 [MARK,NAM(2)] "x"
    @1fe4df68 13 INTSXP g0c0 [] (len=0, tl=0)
    TAG: @2363208 01 SYMSXP g0c0 [MARK,NAM(2),LCK,gp=0x4000] "class" (has value)
    @1fd9f1b8 16 STRSXP g0c1 [NAM(2),ATT] (len=1, tl=0)
      @2e09e138 09 CHARSXP g0c1 [gp=0x61] [ASCII] [cached] "MyClass"
    ATTRIB:
      @1fd9fb20 02 LISTSXP g0c0 [] 
    TAG: @236cc00 01 SYMSXP g0c0 [MARK,NAM(2)] "package"
    @1fd9f278 16 STRSXP g0c1 [NAM(2)] (len=1, tl=0)
      @23cc938 09 CHARSXP g0c2 [MARK,gp=0x61] [ASCII] [cached] ".GlobalEnv"

Which shows that the underlying S-expression used to represent all R objects is an S4SXP, with a list of attributes attached.

By using S3-ism class<- you've created, as @hadley points out, a hybrid monster. class<- merely updates the class attribute, without altering the underlying S4SXP. When you print the object, it prints using the print method for objects of class "animal", probably print.default. On the other hand, isS4 tests whether the S-expression is S4SXP, which it is. So you've got some of each...

Coerce, perhaps by implementing the relevent setAs function, usingas(obj, "animal")`.

OTHER TIPS

It is a little bit tricky to ask what is an S4 object. If we take the definition from R internals, yes, it is still an S4 object because the S4 bit is still set.

obj <- new('MyClass', data=data.frame(1:3), name='An S4 class')
attr(obj, 'class')
## [1] "MyClass"
## attr(,"package")
## [1] ".GlobalEnv"

obj2 <- obj
class(obj2) <- 'animal'
attr(obj, 'class')
## [1] "MyClass"

Note that the only difference (as far as memory representation is concerned) between obj and obj2 is in fact the lack of package attribute associated with the class attribute. We can "fix" this by calling:

attr(class(obj2), "package") <- ".GlobalEnv"

But in such a case we also get the same "strange" result:

print(obj2)
## An object of class "animal"
## <S4 Type Object>
## attr(,"data")
##   X1.3
## 1    1
## 2    2
## 3    3
## attr(,"name")
## [1] "An S4 class"

So let's look for the method responsible for printing obj and obj2. In both cases this is done via show with signature ANY. Printing getMethod("show", "ANY") dispatches us to the showDefault function.

And the first thing that showDefault does is:

...
clDef <- getClass(cl <- class(object), .Force = TRUE)
...

You see, getClass cannot find the formal class definition for animal in the GlobalEnv. This is why it calls show(unclass(object)) and we see everything as attributes (cf. print(unclass(obj))) (EDIT: why attributes: explained in @MartinMorgan's answer).

obj will continue to behave as an S4 object after adding the class attribute 'animal', but note that changing the value of a slot in this hybrid object will fail unless animal is a defined S4 class with a slot of the same name. Additionally, the act of changing a slot value will also drop any slots that aren't in animal.

obj@data <- data.frame() # FAILS, animal not defined
setClass("animal", representation(data="data.frame"))
obj@data <- data.frame() # works, but drops name

As @MartinMorgan points out, the correct way to change one S4 class to another is to register a conversion function with setAs and then call as on the object with the name of the new class:

# define animal with the same slots
setClass("animal", representation(data="data.frame", name="character"))
# register conversion function
setAs("MyClass", "animal", function(from, to )new(to, data=from@data, name=from@name))
# new obj
obj <- new('MyClass', data=data.frame(1:3), name='An S4 class')
as(obj, 'animal')
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top