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:
function(x) {
my_new_generic <-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?
Sys.Date()
x <-s3_dispatch(print(x))
#> => print.Date
#> * print.default
The output here is simple:
=>
indicates the method that is called, hereprint.Date()
*
indicates a method that is defined, but not called, hereprint.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.
matrix(1:10, nrow = 2)
x <-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
Read the source code for
t()
andt.test()
and confirm thatt.test()
is an S3 generic and not an S3 method. What happens if you create an object with classtest
and callt()
with it? Why?structure(1:10, class = "test") x <-t(x)
What generics does the
table
class have methods for?What generics does the
ecdf
class have methods for?Which base generic has the greatest number of defined methods?
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 doesUseMethod()
violate?function(x) { g <- 10 x <- 10 y <-UseMethod("g") } function(x) c(x = x, y = y) g.default <- 1 x <- 1 y <-g(x) #> x y #> 1 10
What are the arguments to
[
? Why is this a hard question to answer?
The exception is internal generics, which are implemented in C, and are the topic of Section 13.7.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.↩︎