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:
We forget to enter the union status.
We misspell union as onion.
We create an object of some class other than "employee"
but accidentally set its class
attribute to "employee"
.
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 |
|
Create object | Build list, set |
|
Reference member variable | $ | @ |
Implement generic | Define |
|
Declare generic |
|
|
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.
> joe Joe has a salary of 55000 and is in the union