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, using
as(obj, "animal")`.