13.4 Generics and methods

The job of an S3 generic is to perform method dispatch, i.e. find the specific implementation for a class. Method dispatch is performed by UseMethod(), which every generic calls48. UseMethod() takes two arguments: the name of the generic function (required), and the argument to use for method dispatch (optional). If you omit the second argument, it will dispatch based on the first argument, which is almost always what is desired.

Most generics are very simple, and consist of only a call to UseMethod(). Take mean() for example:

mean
#> function (x, ...) 
#> UseMethod("mean")
#> <bytecode: 0x55c1199a7480>
#> <environment: namespace:base>

Creating your own generic is similarly simple:

my_new_generic <- function(x) {
  UseMethod("my_new_generic")
}

(If you wonder why we have to repeat my_new_generic twice, think back to Section 6.2.3.)

You don’t pass any of the arguments of the generic to UseMethod(); it uses deep magic to pass to the method automatically. The precise process is complicated and frequently surprising, so you should avoid doing any computation in a generic. To learn the full details, carefully read the Technical Details section in ?UseMethod.

13.4.1 Method dispatch

How does UseMethod() work? It basically creates a vector of method names, paste0("generic", ".", c(class(x), "default")), and then looks for each potential method in turn. We can see this in action with sloop::s3_dispatch(). You give it a call to an S3 generic, and it lists all the possible methods. For example, what method is called when you print a Date object?

x <- Sys.Date()
s3_dispatch(print(x))
#> => print.Date
#>  * print.default

The output here is simple:

  • => indicates the method that is called, here print.Date()
  • * indicates a method that is defined, but not called, here print.default().

The “default” class is a special pseudo-class. This is not a real class, but is included to make it possible to define a standard fallback that is found whenever a class-specific method is not available.

The essence of method dispatch is quite simple, but as the chapter proceeds you’ll see it get progressively more complicated to encompass inheritance, base types, internal generics, and group generics. The code below shows a couple of more complicated cases which we’ll come back to in Sections 14.2.4 and 13.7.

x <- matrix(1:10, nrow = 2)
s3_dispatch(mean(x))
#>    mean.matrix
#>    mean.integer
#>    mean.numeric
#> => mean.default

s3_dispatch(sum(Sys.time()))
#>    sum.POSIXct
#>    sum.POSIXt
#>    sum.default
#> => Summary.POSIXct
#>    Summary.POSIXt
#>    Summary.default
#> -> sum (internal)

13.4.2 Finding methods

sloop::s3_dispatch() lets you find the specific method used for a single call. What if you want to find all methods defined for a generic or associated with a class? That’s the job of sloop::s3_methods_generic() and sloop::s3_methods_class():

s3_methods_generic("mean")
#> # A tibble: 6 x 4
#>   generic class    visible source             
#>   <chr>   <chr>    <lgl>   <chr>              
#> 1 mean    Date     TRUE    base               
#> 2 mean    default  TRUE    base               
#> 3 mean    difftime TRUE    base               
#> 4 mean    POSIXct  TRUE    base               
#> 5 mean    POSIXlt  TRUE    base               
#> 6 mean    quosure  FALSE   registered S3method

s3_methods_class("ordered")
#> # A tibble: 4 x 4
#>   generic       class   visible source             
#>   <chr>         <chr>   <lgl>   <chr>              
#> 1 as.data.frame ordered TRUE    base               
#> 2 Ops           ordered TRUE    base               
#> 3 relevel       ordered FALSE   registered S3method
#> 4 Summary       ordered TRUE    base

13.4.3 Creating methods

There are two wrinkles to be aware of when you create a new method:

  • First, you should only ever write a method if you own the generic or the class. R will allow you to define a method even if you don’t, but it is exceedingly bad manners. Instead, work with the author of either the generic or the class to add the method in their code.

  • A method must have the same arguments as its generic. This is enforced in packages by R CMD check, but it’s good practice even if you’re not creating a package.

    There is one exception to this rule: if the generic has ..., the method can contain a superset of the arguments. This allows methods to take arbitrary additional arguments. The downside of using ..., however, is that any misspelled arguments will be silently swallowed49, as mentioned in Section 6.6.

13.4.4 Exercises

  1. Read the source code for t() and t.test() and confirm that t.test() is an S3 generic and not an S3 method. What happens if you create an object with class test and call t() with it? Why?

    x <- structure(1:10, class = "test")
    t(x)
  2. What generics does the table class have methods for?

  3. What generics does the ecdf class have methods for?

  4. Which base generic has the greatest number of defined methods?

  5. Carefully read the documentation for UseMethod() and explain why the following code returns the results that it does. What two usual rules of function evaluation does UseMethod() violate?

    g <- function(x) {
      x <- 10
      y <- 10
      UseMethod("g")
    }
    g.default <- function(x) c(x = x, y = y)
    
    x <- 1
    y <- 1
    g(x)
    #>  x  y 
    #>  1 10
  6. What are the arguments to [? Why is this a hard question to answer?


  1. The exception is internal generics, which are implemented in C, and are the topic of Section 13.7.2.↩︎

  2. See https://github.com/hadley/ellipsis for an experimental way of warning when methods fail to use all the arguments in ..., providing a potential resolution of this issue.↩︎