15.4 Generics and methods

The job of a generic is to perform method dispatch, i.e. find the specific implementation for the combination of classes passed to the generic. Here you’ll learn how to define S4 generics and methods, then in the next section we’ll explore precisely how S4 method dispatch works.

To create a new S4 generic, call setGeneric() with a function that calls standardGeneric():

setGeneric("myGeneric", function(x) standardGeneric("myGeneric"))

By convention, new S4 generics should use lowerCamelCase.

It is bad practice to use {} in the generic as it triggers a special case that is more expensive, and generally best avoided.

# Don't do this!
setGeneric("myGeneric", function(x) {
  standardGeneric("myGeneric")
})

15.4.1 Signature

Like setClass(), setGeneric() has many other arguments. There is only one that you need to know about: signature. This allows you to control the arguments that are used for method dispatch. If signature is not supplied, all arguments (apart from ...) are used. It is occasionally useful to remove arguments from dispatch. This allows you to require that methods provide arguments like verbose = TRUE or quiet = FALSE, but they don’t take part in dispatch.

setGeneric("myGeneric", 
  function(x, ..., verbose = TRUE) standardGeneric("myGeneric"),
  signature = "x"
)

15.4.2 Methods

A generic isn’t useful without some methods, and in S4 you define methods with setMethod(). There are three important arguments: the name of the generic, the name of the class, and the method itself.

setMethod("myGeneric", "Person", function(x) {
  # method implementation
})

More formally, the second argument to setMethod() is called the signature. In S4, unlike S3, the signature can include multiple arguments. This makes method dispatch in S4 substantially more complicated, but avoids having to implement double-dispatch as a special case. We’ll talk more about multiple dispatch in the next section. setMethod() has other arguments, but you should never use them.

To list all the methods that belong to a generic, or that are associated with a class, use methods("generic") or methods(class = "class"); to find the implementation of a specific method, use selectMethod("generic", "class").

15.4.3 Show method

The most commonly defined S4 method that controls printing is show(), which controls how the object appears when it is printed. To define a method for an existing generic, you must first determine the arguments. You can get those from the documentation or by looking at the args() of the generic:

args(getGeneric("show"))
#> function (object) 
#> NULL

Our show method needs to have a single argument object:

setMethod("show", "Person", function(object) {
  cat(is(object)[[1]], "\n",
      "  Name: ", object@name, "\n",
      "  Age:  ", object@age, "\n",
      sep = ""
  )
})
john
#> Person
#>   Name: John Smith
#>   Age:  50

15.4.4 Accessors

Slots should be considered an internal implementation detail: they can change without warning and user code should avoid accessing them directly. Instead, all user-accessible slots should be accompanied by a pair of accessors. If the slot is unique to the class, this can just be a function:

person_name <- function(x) x@name

Typically, however, you’ll define a generic so that multiple classes can use the same interface:

setGeneric("name", function(x) standardGeneric("name"))
setMethod("name", "Person", function(x) x@name)

name(john)
#> [1] "John Smith"

If the slot is also writeable, you should provide a setter function. You should always include validObject() in the setter to prevent the user from creating invalid objects.

setGeneric("name<-", function(x, value) standardGeneric("name<-"))
setMethod("name<-", "Person", function(x, value) {
  x@name <- value
  validObject(x)
  x
})

name(john) <- "Jon Smythe"
name(john)
#> [1] "Jon Smythe"

name(john) <- letters
#> Error in validObject(x): invalid class "Person" object: @name and @age must be
#> same length

(If the name<- notation is unfamiliar, review Section 6.8.)

15.4.5 Exercises

  1. Add age() accessors for the Person class.

  2. In the definition of the generic, why is it necessary to repeat the name of the generic twice?

  3. Why does the show() method defined in Section 15.4.3 use is(object)[[1]]? (Hint: try printing the employee subclass.)

  4. What happens if you define a method with different argument names to the generic?