S4 Classes

Some programmers feel that S3 does not provide the safety normally associated with OOP. For example, consider our earlier employee database example, where our class "employee" had three fields: name, salary, and union. Here are some possible mishaps:

In each of these cases, R will not complain. The goal of S4 is to elicit a complaint and prevent such accidents.

S4 structures are considerably richer than S3 structures, but here we present just the basics. Table 9-1 shows an overview of the differences between the two classes.

Table 9-1. Basic R Operators

Operation

S3

S4

Define class

Implicit in constructor code

setClass()

Create object

Build list, set class attr

new()

Reference member variable

$

@

Implement generic f()

Define f.classname()

setMethod()

Declare generic

UseMethod()

setGeneric()

You define an S4 class by calling setClass(). Continuing our employee example, we could write the following:

> setClass("employee",
+    representation(
+       name="character",
+       salary="numeric",
+       union="logical")
+ )
[1] "employee"

This defines a new class, "employee", with three member variables of the specified types.

Now let’s create an instance of this class, for Joe, using new(), a built-in constructor function for S4 classes:

> joe <- new("employee",name="Joe",salary=55000,union=T)
> joe
An object of class "employee"
Slot "name":
[1] "Joe"
Slot "salary":
[1] 55000

Slot "union":
[1] TRUE

Note that the member variables are called slots, referenced via the @ symbol. Here’s an example:

> joe@salary
[1] 55000

We can also use the slot() function, say, as another way to query Joe’s salary:

> slot(joe,"salary")
[1] 55000

We can assign components similarly. Let’s give Joe a raise:

> joe@salary <- 65000
> joe
An object of class "employee"
Slot "name":
[1] "Joe"

Slot "salary":
[1] 65000

Slot "union":
[1] TRUE

Nah, he deserves a bigger raise that that:

> slot(joe,"salary") <- 88000
> joe
An object of class "employee"
Slot "name":
[1] "Joe"

Slot "salary":
[1] 88000

Slot "union":
[1] TRUE

As noted, an advantage of using S4 is safety. To illustrate this, suppose we were to accidentally spell salary as salry, like this:

> joe@salry <- 48000
Error in checkSlotAssignment(object, name, value) :
  "salry" is not a slot in class "employee"

By contrast, in S3 there would be no error message. S3 classes are just lists, and you are allowed to add a new component (deliberately or not) at any time.

To define an implementation of a generic function on an S4 class, use setMethod(). Let’s do that for our class "employee" here. We’ll implement the show() function, which is the S4 analog of S3’s generic "print".

As you know, in R, when you type the name of a variable while in interactive mode, the value of the variable is printed out:

> joe
An object of class "employee"
Slot "name":
[1] "Joe"

Slot "salary":
[1] 88000

Slot "union":
[1] TRUE

Since joe is an S4 object, the action here is that show() is called. In fact, we would get the same output by typing this:

> show(joe)

Let’s override that, with the following code:

setMethod("show", "employee",
   function(object) {
      inorout <- ifelse(object@union,"is","is not")
      cat(object@name,"has a salary of",object@salary,
         "and",inorout, "in the union", "\n")
   }
)

The first argument gives the name of the generic function for which we will define a class-specific method, and the second argument gives the class name. We then define the new function.

Let’s try it out:

> joe
Joe has a salary of 55000 and is in the union