13.7 Dispatch details

This chapter concludes with a few additional details about method dispatch. It is safe to skip these details if you’re new to S3.

13.7.1 S3 and base types

What happens when you call an S3 generic with a base object, i.e. an object with no class? You might think it would dispatch on what class() returns:

#> [1] "matrix" "array"

But unfortunately dispatch actually occurs on the implicit class, which has three components:

  • The string “array” or “matrix” if the object has dimensions
  • The result of typeof() with a few minor tweaks
  • The string “numeric” if object is “integer” or “double”

There is no base function that will compute the implicit class, but you can use sloop::s3_class()

#> [1] "matrix"  "integer" "numeric"

This is used by s3_dispatch():

#>    print.matrix
#>    print.integer
#>    print.numeric
#> => print.default

This means that the class() of an object does not uniquely determine its dispatch:

x1 <- 1:5
#> [1] "integer"
#>    mean.integer
#>    mean.numeric
#> => mean.default

x2 <- structure(x1, class = "integer")
#> [1] "integer"
#>    mean.integer
#> => mean.default

13.7.2 Internal generics

Some base functions, like [, sum(), and cbind(), are called internal generics because they don’t call UseMethod() but instead call the C functions DispatchGroup() or DispatchOrEval(). s3_dispatch() shows internal generics by including the name of the generic followed by (internal):

#> => [.POSIXct
#>    [.POSIXt
#>    [.default
#> -> [ (internal)

For performance reasons, internal generics do not dispatch to methods unless the class attribute has been set, which means that internal generics do not use the implicit class. Again, if you’re ever confused about method dispatch, you can rely on s3_dispatch().

13.7.3 Group generics

Group generics are the most complicated part of S3 method dispatch because they involve both NextMethod() and internal generics. Like internal generics, they only exist in base R, and you cannot define your own group generic.

There are four group generics:

  • Math: abs(), sign(), sqrt(), floor(), cos(), sin(), log(), and more (see ?Math for the complete list).

  • Ops: +, -, *, /, ^, %%, %/%, &, |, !, ==, !=, <, <=, >=, and >.

  • Summary: all(), any(), sum(), prod(), min(), max(), and range().

  • Complex: Arg(), Conj(), Im(), Mod(), Re().

Defining a single group generic for your class overrides the default behaviour for all of the members of the group. Methods for group generics are looked for only if the methods for the specific generic do not exist:

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

Most group generics involve a call to NextMethod(). For example, take difftime() objects. If you look at the method dispatch for abs(), you’ll see there’s a Math group generic defined.

y <- as.difftime(10, units = "mins")
#>    abs.difftime
#>    abs.default
#> => Math.difftime
#>    Math.default
#> -> abs (internal)

Math.difftime basically looks like this:

Math.difftime <- function(x, ...) {
  new_difftime(NextMethod(), units = attr(x, "units"))

It dispatches to the next method, here the internal default, to perform the actual computation, then restore the class and attributes. (To better support subclasses of difftime this would need to call vec_restore(), as described in Section 13.6.2.)

Inside a group generic function a special variable .Generic provides the actual generic function called. This can be useful when producing error messages, and can sometimes be useful if you need to manually re-call the generic with different arguments.

13.7.4 Double dispatch

Generics in the Ops group, which includes the two-argument arithmetic and Boolean operators like - and &, implement a special type of method dispatch. They dispatch on the type of both of the arguments, which is called double dispatch. This is necessary to preserve the commutative property of many operators, i.e. a + b should equal b + a. Take the following simple example:

date <- as.Date("2017-01-01")
integer <- 1L

date + integer
#> [1] "2017-01-02"
integer + date
#> [1] "2017-01-02"

If + dispatched only on the first argument, it would return different values for the two cases. To overcome this problem, generics in the Ops group use a slightly different strategy from usual. Rather than doing a single method dispatch, they do two, one for each input. There are three possible outcomes of this lookup:

  • The methods are the same, so it doesn’t matter which method is used.

  • The methods are different, and R falls back to the internal method with a warning.

  • One method is internal, in which case R calls the other method.

This approach is error prone so if you want to implement robust double dispatch for algebraic operators, I recommend using the vctrs package. See ?vctrs::vec_arith for details.

13.7.5 Exercises

  1. Explain the differences in dispatch below:

    length.integer <- function(x) 10
    x1 <- 1:5
    #> [1] "integer"
    #>  * length.integer
    #>    length.numeric
    #>    length.default
    #> => length (internal)
    x2 <- structure(x1, class = "integer")
    #> [1] "integer"
    #> => length.integer
    #>    length.default
    #>  * length (internal)
  2. What classes have a method for the Math group generic in base R? Read the source code. How do the methods work?

  3. Math.difftime() is more complicated than I described. Why?