13 S3

Attaching the needed libraries:

library(sloop, warn.conflicts = FALSE)
library(dplyr, warn.conflicts = FALSE)
library(purrr, warn.conflicts = FALSE)

13.1 Basics (Exercises 13.2.1)


Q1. Describe the difference between t.test() and t.data.frame(). When is each function called?

A1. The difference between t.test() and t.data.frame() is the following:

  • t.test() is a generic function to perform a t-test.

  • t.data.frame() is a method for generic t() (a matrix transform function) and will be dispatched for data.frame objects.

We can also confirm these function types using ftype():

ftype(t.test)
#> [1] "S3"      "generic"
ftype(t.data.frame)
#> [1] "S3"     "method"

Q2. Make a list of commonly used base R functions that contain . in their name but are not S3 methods.

A2. Here are a few common R functions with . but that are not S3 methods:

For example,

ftype(as.data.frame)
#> [1] "S3"      "generic"
ftype(on.exit)
#> [1] "primitive"

Q3. What does the as.data.frame.data.frame() method do? Why is it confusing? How could you avoid this confusion in your own code?

A3. It’s an S3 method for generic as.data.frame().

ftype(as.data.frame.data.frame)
#> [1] "S3"     "method"

It can be seen in all methods supported by this generic:

s3_methods_generic("as.data.frame") %>%
  dplyr::filter(class == "data.frame")
#> # A tibble: 1 × 4
#>   generic       class      visible source
#>   <chr>         <chr>      <lgl>   <chr> 
#> 1 as.data.frame data.frame TRUE    base

Given the number of .s in this name, it is quite confusing to figure out what is the name of the generic and the name of the class.


Q4. Describe the difference in behaviour in these two calls.

set.seed(1014)
some_days <- as.Date("2017-01-31") + sample(10, 5)
mean(some_days)
#> [1] "2017-02-06"
mean(unclass(some_days))
#> [1] 17203.4

A4. The difference in behaviour in the specified calls.

  • Before unclassing, the mean generic dispatches .Date method:
some_days <- as.Date("2017-01-31") + sample(10, 5)

some_days
#> [1] "2017-02-06" "2017-02-09" "2017-02-05" "2017-02-08"
#> [5] "2017-02-07"

s3_dispatch(mean(some_days))
#> => mean.Date
#>  * mean.default

mean(some_days)
#> [1] "2017-02-07"
  • After unclassing, the mean generic dispatches .numeric method:
unclass(some_days)
#> [1] 17203 17206 17202 17205 17204

mean(unclass(some_days))
#> [1] 17204

s3_dispatch(mean(unclass(some_days)))
#>    mean.double
#>    mean.numeric
#> => mean.default

Q5. What class of object does the following code return? What base type is it built on? What attributes does it use?

x <- ecdf(rpois(100, 10))
x

A5. The object is based on base type closure6, which is a type of function.

x <- ecdf(rpois(100, 10))
x
#> Empirical CDF 
#> Call: ecdf(rpois(100, 10))
#>  x[1:18] =      2,      3,      4,  ...,     18,     19

otype(x)
#> [1] "S3"
typeof(x)
#> [1] "closure"

Its class is ecdf, which has other superclasses.

s3_class(x)
#> [1] "ecdf"     "stepfun"  "function"

Apart from class, it has the following attributes:

attributes(x)
#> $class
#> [1] "ecdf"     "stepfun"  "function"
#> 
#> $call
#> ecdf(rpois(100, 10))

Q6. What class of object does the following code return? What base type is it built on? What attributes does it use?

x <- table(rpois(100, 5))
x

A6. The object is based on base type integer.

x <- table(rpois(100, 5))
x
#> 
#>  1  2  3  4  5  6  7  8  9 10 
#>  7  7 18 13 14 14 16  4  4  3

otype(x)
#> [1] "S3"
typeof(x)
#> [1] "integer"

Its class is table.

s3_class(x)
#> [1] "table"

Apart from class, it has the following attributes:

attributes(x)
#> $dim
#> [1] 10
#> 
#> $dimnames
#> $dimnames[[1]]
#>  [1] "1"  "2"  "3"  "4"  "5"  "6"  "7"  "8"  "9"  "10"
#> 
#> 
#> $class
#> [1] "table"

13.2 Classes (Exercises 13.3.4)


Q1. Write a constructor for data.frame objects. What base type is a data frame built on? What attributes does it use? What are the restrictions placed on the individual elements? What about the names?

A1. A data frame is built on top of a named list of atomic vectors and has attributes for row names:

unclass(data.frame())
#> named list()
#> attr(,"row.names")
#> integer(0)

The restriction imposed on individual elements is that they need to have the same length. Additionally, the names need to be syntactically valid and unique.

new_data_frame <- function(x = list(), row.names = character()) {
  # row names should be character
  if (!all(is.character(row.names))) {
    stop("Row name should be of `chracter` type.", call. = FALSE)
  }

  # all elements should have the same length
  unique_element_lengths <- unique(purrr::map_int(x, length))
  if (length(unique_element_lengths) > 1L) {
    stop("All list elements in `x` should have same length.", call. = FALSE)
  }

  # if not provided, generate row names
  # this is necessary if there is at least one element in the list
  if (length(x) > 0L && length(row.names) == 0L) {
    row.names <- .set_row_names(unique_element_lengths)
  }

  structure(x, class = "data.frame", row.names = row.names)
}

Let’s try it out:

new_data_frame(list("x" = 1, "y" = c(2, 3)))
#> Error: All list elements in `x` should have same length.

new_data_frame(list("x" = 1, "y" = c(2)), row.names = 1L)
#> Error: Row name should be of `chracter` type.

new_data_frame(list())
#> data frame with 0 columns and 0 rows

new_data_frame(list("x" = 1, "y" = 2))
#>   x y
#> 1 1 2

new_data_frame(list("x" = 1, "y" = 2), row.names = "row-1")
#>       x y
#> row-1 1 2

Q2. Enhance my factor() helper to have better behaviour when one or more values is not found in levels. What does base::factor() do in this situation?

A2. When one or more values is not found in levels, those values are converted to NA in base::factor():

base::factor(c("a", "b", "c"), levels = c("a", "c"))
#> [1] a    <NA> c   
#> Levels: a c

In the new constructor, we can throw an error to inform the user:

new_factor <- function(x = integer(), levels = character()) {
  stopifnot(is.integer(x))
  stopifnot(is.character(levels))

  structure(
    x,
    levels = levels,
    class = "factor"
  )
}

validate_factor <- function(x) {
  values <- unclass(x)
  levels <- attr(x, "levels")

  if (!all(!is.na(values) & values > 0)) {
    stop(
      "All `x` values must be non-missing and greater than zero",
      call. = FALSE
    )
  }

  if (length(levels) < max(values)) {
    stop(
      "There must be at least as many `levels` as possible values in `x`",
      call. = FALSE
    )
  }

  x
}

create_factor <- function(x = character(), levels = unique(x)) {
  ind <- match(x, levels)

  if (any(is.na(ind))) {
    missing_values <- x[which(is.na(match(x, levels)))]

    stop(
      paste0(
        "Following values from `x` are not present in `levels`:\n",
        paste0(missing_values, collapse = "\n")
      ),
      call. = FALSE
    )
  }

  validate_factor(new_factor(ind, levels))
}

Let’s try it out:

create_factor(c("a", "b", "c"), levels = c("a", "c"))
#> Error: Following values from `x` are not present in `levels`:
#> b

create_factor(c("a", "b", "c"), levels = c("a", "b", "c"))
#> [1] a b c
#> Levels: a b c

Q3. Carefully read the source code of factor(). What does it do that my constructor does not?

A3. The source code for factor() can be read here.

There are a number ways in which the base version is more flexible.

  • It allows labeling the values:
x <- c("a", "b", "b")
levels <- c("a", "b", "c")
labels <- c("one", "two", "three")

factor(x, levels = levels, labels = labels)
#> [1] one two two
#> Levels: one two three
  • It checks that the levels are not duplicated.
x <- c("a", "b", "b")
levels <- c("a", "b", "b")

factor(x, levels = levels)
#> Error in `levels<-`(`*tmp*`, value = as.character(levels)): factor level [3] is duplicated

create_factor(x, levels = levels)
#> [1] a b b
#> Levels: a b b
#> Warning in print.factor(x): duplicated level [3] in factor
  • The levels argument can be NULL.
x <- c("a", "b", "b")

factor(x, levels = NULL)
#> [1] <NA> <NA> <NA>
#> Levels:

create_factor(x, levels = NULL)
#> Error: Following values from `x` are not present in `levels`:
#> a
#> b
#> b

Q4. Factors have an optional “contrasts” attribute. Read the help for C(), and briefly describe the purpose of the attribute. What type should it have? Rewrite the new_factor() constructor to include this attribute.

A4. Categorical variables are typically encoded as dummy variables in regression models and by default each level is compared with the first factor level. Contrats provide a flexible way for such comparisons.

You can set the "contrasts" attribute for a factor using stats::C().

Alternatively, you can set the "contrasts" attribute using matrix (?contrasts):

[Contrasts] can be a matrix with one row for each level of the factor or a suitable function like contr.poly or a character string giving the name of the function

The constructor provided in the book:

new_factor <- function(x = integer(), levels = character()) {
  stopifnot(is.integer(x))
  stopifnot(is.character(levels))

  structure(
    x,
    levels = levels,
    class = "factor"
  )
}

Here is how it can be updated to also support contrasts:

new_factor <- function(x = integer(),
                       levels = character(),
                       contrasts = NULL) {
  stopifnot(is.integer(x))
  stopifnot(is.character(levels))

  if (!is.null(contrasts)) {
    stopifnot(is.matrix(contrasts) && is.numeric(contrasts))
  }

  structure(
    x,
    levels = levels,
    class = "factor",
    contrasts = contrasts
  )
}

Q5. Read the documentation for utils::as.roman(). How would you write a constructor for this class? Does it need a validator? What might a helper do?

A5. utils::as.roman() converts Indo-Arabic numerals to Roman numerals. Removing its class also reveals that it is implemented using the base type integer:

as.roman(1)
#> [1] I

typeof(unclass(as.roman(1)))
#> [1] "integer"

Therefore, we can create a simple constructor to create a new instance of this class:

new_roman <- function(x = integer()) {
  stopifnot(is.integer(x))

  structure(x, class = "roman")
}

The docs mention the following:

Only numbers between 1 and 3899 have a unique representation as roman numbers, and hence others result in as.roman(NA).

as.roman(10000)
#> [1] <NA>

Therefore, we can warn the user and then return NA in a validator function:

validate_new_roman <- function(x) {
  int_values <- unclass(x)

  if (any(int_values < 1L | int_values > 3899L)) {
    warning(
      "Integer should be between 1 and 3899. Returning `NA` otherwise.",
      call. = FALSE
    )
  }

  x
}

The helper function can coerce the entered input to integer type for convenience:

roman <- function(x = integer()) {
  x <- as.integer(x)

  validate_new_roman(new_roman(x))
}

Let’s try it out:

roman(1)
#> [1] I

roman(c(5, 20, 100, 150, 100000))
#> Warning: Integer should be between 1 and 3899. Returning
#> `NA` otherwise.
#> [1] V    XX   C    CL   <NA>

13.3 Generics and methods (Exercises 13.4.4)

Q1. 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)

A1. Looking at source code of these functions, we can see that both of these are generic, and we can confirm the same using sloop:

t
#> function (x) 
#> UseMethod("t")
#> <bytecode: 0x55e1869730c0>
#> <environment: namespace:base>
sloop::is_s3_generic("t")
#> [1] TRUE

t.test
#> function (x, ...) 
#> UseMethod("t.test")
#> <bytecode: 0x55e183597350>
#> <environment: namespace:stats>
sloop::is_s3_generic("t.test")
#> [1] TRUE

Looking at the S3 dispatch, we can see that since R can’t find S3 method for test class for generic function t(), it dispatches the default method, which converts the structure to a matrix:

x <- structure(1:10, class = "test")
t(x)
#>      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
#> [1,]    1    2    3    4    5    6    7    8    9    10
#> attr(,"class")
#> [1] "test"
s3_dispatch(t(x))
#>    t.test
#> => t.default

The same behaviour can be observed with a vector:

t(1:10)
#>      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
#> [1,]    1    2    3    4    5    6    7    8    9    10

Q2. What generics does the table class have methods for?

A2. The table class have methods for the following generics:

s3_methods_class("table")
#> # A tibble: 11 × 4
#>    generic       class visible source             
#>    <chr>         <chr> <lgl>   <chr>              
#>  1 [             table TRUE    base               
#>  2 aperm         table TRUE    base               
#>  3 as_tibble     table FALSE   registered S3method
#>  4 as.data.frame table TRUE    base               
#>  5 Axis          table FALSE   registered S3method
#>  6 lines         table FALSE   registered S3method
#>  7 plot          table FALSE   registered S3method
#>  8 points        table FALSE   registered S3method
#>  9 print         table TRUE    base               
#> 10 summary       table TRUE    base               
#> 11 tail          table FALSE   registered S3method

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

A3. The ecdf class have methods for the following generics:

s3_methods_class("ecdf")
#> # A tibble: 4 × 4
#>   generic  class visible source             
#>   <chr>    <chr> <lgl>   <chr>              
#> 1 plot     ecdf  TRUE    stats              
#> 2 print    ecdf  FALSE   registered S3method
#> 3 quantile ecdf  FALSE   registered S3method
#> 4 summary  ecdf  FALSE   registered S3method

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

A4. To answer this question, first, let’s list all functions base has and only retain the generics.

# getting all functions names
objs <- mget(ls("package:base", all = TRUE), inherits = TRUE)
funs <- Filter(is.function, objs)

# extracting only generics
genFuns <- names(funs) %>%
  purrr::keep(~ sloop::is_s3_generic(.x))

Now it’s a simple matter of counting number of methods per generic and ordering the data frame in descending order of this count:

purrr::map_dfr(
  genFuns,
  ~ s3_methods_generic(.)
) %>%
  dplyr::group_by(generic) %>%
  dplyr::tally() %>%
  dplyr::arrange(desc(n))
#> # A tibble: 123 × 2
#>    generic           n
#>    <chr>         <int>
#>  1 print           291
#>  2 format          132
#>  3 [                53
#>  4 summary          39
#>  5 as.character     38
#>  6 as.data.frame    32
#>  7 plot             31
#>  8 [[               26
#>  9 [<-              17
#> 10 $                15
#> # ℹ 113 more rows

This reveals that the base generic function with most methods is print().

Q5. 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 1

A5. If called directly, g.default() method takes x value from argument and y from the global environment:

g.default(x)
#> x y 
#> 1 1

But, if g() function is called, it takes the x from argument, but comes from function environment:

g(x)
#> x y 
#> 1 1

The docs for ?UseMethod() clarify why this is the case:

Any local variables defined before the call to UseMethod are retained

That is, when UseMethod() calls g.default(), variables defined inside the generic are also available to g.default() method. The arguments supplied to the function are passed on as is, however, and cannot be affected by code inside the generic.

Two rules of function evaluation violated by UseMethod():

  • Name masking
  • A fresh start

Q6. What are the arguments to [? Why is this a hard question to answer?

A6. It is difficult to say how many formal arguments the subsetting [ operator has because it is a generic function with methods for vectors, matrices, arrays, lists, etc., and these different methods have different number of arguments:

s3_methods_generic("[") %>%
  dplyr::filter(source == "base")
#> # A tibble: 17 × 4
#>    generic class           visible source
#>    <chr>   <chr>           <lgl>   <chr> 
#>  1 [       AsIs            TRUE    base  
#>  2 [       data.frame      TRUE    base  
#>  3 [       Date            TRUE    base  
#>  4 [       difftime        TRUE    base  
#>  5 [       Dlist           TRUE    base  
#>  6 [       DLLInfoList     TRUE    base  
#>  7 [       factor          TRUE    base  
#>  8 [       hexmode         TRUE    base  
#>  9 [       listof          TRUE    base  
#> 10 [       noquote         TRUE    base  
#> 11 [       numeric_version TRUE    base  
#> 12 [       octmode         TRUE    base  
#> 13 [       POSIXct         TRUE    base  
#> 14 [       POSIXlt         TRUE    base  
#> 15 [       simple.list     TRUE    base  
#> 16 [       table           TRUE    base  
#> 17 [       warnings        TRUE    base

We can sample a few of them to see the wide variation in the number of formal arguments:

# table
names(formals(`[.table`))
#> [1] "x"    "i"    "j"    "..."  "drop"

# Date
names(formals(`[.Date`))
#> [1] "x"    "..."  "drop"

# data frame
names(formals(`[.data.frame`))
#> [1] "x"    "i"    "j"    "drop"

# etc.

13.4 Object styles (Exercises 13.5.1)

Q1. Categorise the objects returned by lm(), factor(), table(), as.Date(), as.POSIXct() ecdf(), ordered(), I() into the styles described above.

A1. Objects returned by these functions can be categorized as follows:

  • Vector style objects (length represents no. of observations)

factor()

factor_obj <- factor(c("a", "b"))
length(factor_obj)
#> [1] 2
length(unclass(factor_obj))
#> [1] 2

table()

tab_object <- table(mtcars$am)
length(tab_object)
#> [1] 2
length(unlist(tab_object))
#> [1] 2

as.Date()

date_obj <- as.Date("02/27/92", "%m/%d/%y")
length(date_obj)
#> [1] 1
length(unclass(date_obj))
#> [1] 1

as.POSIXct()

posix_obj <- as.POSIXct(1472562988, origin = "1960-01-01")
length(posix_obj)
#> [1] 1
length(unclass(posix_obj))
#> [1] 1

ordered()

ordered_obj <- ordered(factor(c("a", "b")))
length(ordered_obj)
#> [1] 2
length(unclass(ordered_obj))
#> [1] 2
  • Record style objects (equi-length vectors to represent object components)

None.

  • Dataframe style objects (Record style but two-dimensions)

None.

  • Scalar objects (a list to represent a single thing)

lm() (represent one regression model)

lm_obj <- lm(wt ~ mpg, mtcars)
length(lm_obj)
#> [1] 12
length(unclass(lm_obj))
#> [1] 12

ecdf() (represents one distribution)

ecdf_obj <- ecdf(rnorm(12))
length(ecdf_obj)
#> [1] 1
length(unclass(ecdf_obj))
#> [1] 1

I() is special: It just adds a new class to the object to indicate that it should be treated as is.

x <- ecdf(rnorm(12))
class(x)
#> [1] "ecdf"     "stepfun"  "function"
class(I(x))
#> [1] "AsIs"     "ecdf"     "stepfun"  "function"

Therefore, the object style would be the same as the superclass’ object style.

Q2. What would a constructor function for lm objects, new_lm(), look like? Use ?lm and experimentation to figure out the required fields and their types.

A2. The lm object is a scalar object, i.e. this object contains a named list of atomic vectors of varying lengths and types to represent a single thing (a regression model).

mod <- lm(wt ~ mpg, mtcars)

typeof(mod)
#> [1] "list"

attributes(mod)
#> $names
#>  [1] "coefficients"  "residuals"     "effects"      
#>  [4] "rank"          "fitted.values" "assign"       
#>  [7] "qr"            "df.residual"   "xlevels"      
#> [10] "call"          "terms"         "model"        
#> 
#> $class
#> [1] "lm"

purrr::map_chr(unclass(mod), typeof)
#>  coefficients     residuals       effects          rank 
#>      "double"      "double"      "double"     "integer" 
#> fitted.values        assign            qr   df.residual 
#>      "double"     "integer"        "list"     "integer" 
#>       xlevels          call         terms         model 
#>        "list"    "language"    "language"        "list"

purrr::map_int(unclass(mod), length)
#>  coefficients     residuals       effects          rank 
#>             2            32            32             1 
#> fitted.values        assign            qr   df.residual 
#>            32             2             5             1 
#>       xlevels          call         terms         model 
#>             0             3             3             2

Based on this information, we can write a new constructor for this object:

new_lm <- function(coefficients,
                   residuals,
                   effects,
                   rank,
                   fitted.values,
                   assign,
                   qr,
                   df.residual,
                   xlevels,
                   call,
                   terms,
                   model) {
  stopifnot(
    is.double(coefficients),
    is.double(residuals),
    is.double(effects),
    is.integer(rank),
    is.double(fitted.values),
    is.integer(assign),
    is.list(qr),
    is.integer(df.residual),
    is.list(xlevels),
    is.language(call),
    is.language(terms),
    is.list(model)
  )

  structure(
    list(
      coefficients  = coefficients,
      residuals     = residuals,
      effects       = effects,
      rank          = rank,
      fitted.values = fitted.values,
      assign        = assign,
      qr            = qr,
      df.residual   = df.residual,
      xlevels       = xlevels,
      call          = call,
      terms         = terms,
      model         = model
    ),
    class = "lm"
  )
}

13.5 Inheritance (Exercises 13.6.3)

Q1. How does [.Date support subclasses? How does it fail to support subclasses?

A1. The [.Date method is defined as follows:

sloop::s3_get_method("[.Date")
#> function (x, ..., drop = TRUE) 
#> {
#>     .Date(NextMethod("["), oldClass(x))
#> }
#> <bytecode: 0x55e186819b78>
#> <environment: namespace:base>

The .Date function looks like this:

.Date
#> function (xx, cl = "Date") 
#> `class<-`(xx, cl)
#> <bytecode: 0x55e1876fce70>
#> <environment: namespace:base>

Here, oldClass is the same as class().

Therefore, by reading this code, we can surmise that:

  • [.Date supports subclasses by preserving the class of the input.
  • [.Date fails to support subclasses by not preserving the attributes of the input.

For example,

x <- structure(Sys.Date(), name = "myName", class = c("subDate", "Date"))

# `$name` is gone
attributes(x[1])
#> $class
#> [1] "subDate" "Date"

x[1]
#> [1] "2024-05-20"

Q2. R has two classes for representing date time data, POSIXct and POSIXlt, which both inherit from POSIXt. Which generics have different behaviours for the two classes? Which generics share the same behaviour?

A2. First, let’s demonstrate that POSIXct and POSIXlt are indeed subclasses and POSIXt is the superclass.

dt_lt <- as.POSIXlt(Sys.time(), "GMT")
class(dt_lt)
#> [1] "POSIXlt" "POSIXt"

dt_ct <- as.POSIXct(Sys.time(), "GMT")
class(dt_ct)
#> [1] "POSIXct" "POSIXt"

dt_t <- structure(dt_ct, class = "POSIXt")
class(dt_t)
#> [1] "POSIXt"

Remember that the way S3 method dispatch works, if a generic has a method for superclass, then that method is also inherited by the subclass.

We can extract a vector of all generics supported by both sub- and super-classes:

(t_generics <- s3_methods_class("POSIXt")$generic)
#>  [1] "-"            "+"            "all.equal"   
#>  [4] "as.character" "Axis"         "cut"         
#>  [7] "diff"         "hist"         "is.numeric"  
#> [10] "julian"       "Math"         "months"      
#> [13] "Ops"          "pretty"       "quantile"    
#> [16] "quarters"     "round"        "seq"         
#> [19] "str"          "trunc"        "weekdays"

(lt_generics <- s3_methods_class("POSIXlt")$generic)
#>  [1] "["             "[["            "[[<-"         
#>  [4] "[<-"           "$<-"           "anyNA"        
#>  [7] "as.data.frame" "as.Date"       "as.double"    
#> [10] "as.list"       "as.matrix"     "as.POSIXct"   
#> [13] "as.vector"     "c"             "duplicated"   
#> [16] "format"        "is.finite"     "is.infinite"  
#> [19] "is.na"         "is.nan"        "length"       
#> [22] "length<-"      "mean"          "mtfrm"        
#> [25] "names"         "names<-"       "print"        
#> [28] "rep"           "sort"          "summary"      
#> [31] "Summary"       "unique"        "weighted.mean"
#> [34] "xtfrm"

(ct_generics <- s3_methods_class("POSIXct")$generic)
#>  [1] "["             "[["            "[<-"          
#>  [4] "as.data.frame" "as.Date"       "as.list"      
#>  [7] "as.POSIXlt"    "c"             "format"       
#> [10] "length<-"      "mean"          "mtfrm"        
#> [13] "print"         "range"         "rep"          
#> [16] "split"         "summary"       "Summary"      
#> [19] "weighted.mean" "xtfrm"

Methods which are specific to the subclasses:

union(lt_generics, ct_generics)
#>  [1] "["             "[["            "[[<-"         
#>  [4] "[<-"           "$<-"           "anyNA"        
#>  [7] "as.data.frame" "as.Date"       "as.double"    
#> [10] "as.list"       "as.matrix"     "as.POSIXct"   
#> [13] "as.vector"     "c"             "duplicated"   
#> [16] "format"        "is.finite"     "is.infinite"  
#> [19] "is.na"         "is.nan"        "length"       
#> [22] "length<-"      "mean"          "mtfrm"        
#> [25] "names"         "names<-"       "print"        
#> [28] "rep"           "sort"          "summary"      
#> [31] "Summary"       "unique"        "weighted.mean"
#> [34] "xtfrm"         "as.POSIXlt"    "range"        
#> [37] "split"

Let’s see an example:

s3_dispatch(is.na(dt_lt))
#> => is.na.POSIXlt
#>    is.na.POSIXt
#>    is.na.default
#>  * is.na (internal)

s3_dispatch(is.na(dt_ct))
#>    is.na.POSIXct
#>    is.na.POSIXt
#>    is.na.default
#> => is.na (internal)

s3_dispatch(is.na(dt_t))
#>    is.na.POSIXt
#>    is.na.default
#> => is.na (internal)

Methods which are inherited by subclasses from superclass:

setdiff(t_generics, union(lt_generics, ct_generics))
#>  [1] "-"            "+"            "all.equal"   
#>  [4] "as.character" "Axis"         "cut"         
#>  [7] "diff"         "hist"         "is.numeric"  
#> [10] "julian"       "Math"         "months"      
#> [13] "Ops"          "pretty"       "quantile"    
#> [16] "quarters"     "round"        "seq"         
#> [19] "str"          "trunc"        "weekdays"

Let’s see one example generic:

s3_dispatch(is.numeric(dt_lt))
#>    is.numeric.POSIXlt
#> => is.numeric.POSIXt
#>    is.numeric.default
#>  * is.numeric (internal)

s3_dispatch(is.numeric(dt_ct))
#>    is.numeric.POSIXct
#> => is.numeric.POSIXt
#>    is.numeric.default
#>  * is.numeric (internal)

s3_dispatch(is.numeric(dt_t))
#> => is.numeric.POSIXt
#>    is.numeric.default
#>  * is.numeric (internal)

Q3. What do you expect this code to return? What does it actually return? Why?

generic2 <- function(x) UseMethod("generic2")
generic2.a1 <- function(x) "a1"
generic2.a2 <- function(x) "a2"
generic2.b <- function(x) {
  class(x) <- "a1"
  NextMethod()
}

generic2(structure(list(), class = c("b", "a2")))

A3. Naively, we would expect for this code to return "a1", but it actually returns "a2":

generic2 <- function(x) UseMethod("generic2")
generic2.a1 <- function(x) "a1"
generic2.a2 <- function(x) "a2"
generic2.b <- function(x) {
  class(x) <- "a1"
  NextMethod()
}

generic2(structure(list(), class = c("b", "a2")))
#> [1] "a2"

S3 dispatch explains why:

sloop::s3_dispatch(generic2(structure(list(), class = c("b", "a2"))))
#> => generic2.b
#> -> generic2.a2
#>    generic2.default

As mentioned in the book, the UseMethod() function

tracks the list of potential next methods with a special variable, which means that modifying the object that’s being dispatched upon will have no impact on which method gets called next.

This special variable is .Class:

.Class is a character vector of classes used to find the next method. NextMethod adds an attribute “previous” to .Class giving the .Class last used for dispatch, and shifts .Class along to that used for dispatch.

So, we can print .Class to confirm that adding a new class to x indeed doesn’t change .Class, and therefore dispatch occurs on "a2" class:

generic2.b <- function(x) {
  message(paste0("before: ", paste0(.Class, collapse = ", ")))
  class(x) <- "a1"
  message(paste0("after: ", paste0(.Class, collapse = ", ")))

  NextMethod()
}

invisible(generic2(structure(list(), class = c("b", "a2"))))
#> before: b, a2
#> after: b, a2

13.6 Dispatch details (Exercises 13.7.5)

Q1. Explain the differences in dispatch below:

length.integer <- function(x) 10

x1 <- 1:5
class(x1)
#> [1] "integer"
s3_dispatch(length(x1))
#>  * length.integer
#>    length.numeric
#>    length.default
#> => length (internal)

x2 <- structure(x1, class = "integer")
class(x2)
#> [1] "integer"
s3_dispatch(length(x2))
#> => length.integer
#>    length.default
#>  * length (internal)

A1. The differences in the dispatch are due to classes of arguments:

s3_class(x1)
#> [1] "integer" "numeric"

s3_class(x2)
#> [1] "integer"

x1 has an implicit class integer but it inherits from numeric, while x2 is explicitly assigned the class integer.

Q2. What classes have a method for the Math group generic in base R? Read the source code. How do the methods work?

A2. The following classes have a method for the Math group generic in base R:

s3_methods_generic("Math") %>%
  dplyr::filter(source == "base")
#> # A tibble: 5 × 4
#>   generic class      visible source
#>   <chr>   <chr>      <lgl>   <chr> 
#> 1 Math    data.frame TRUE    base  
#> 2 Math    Date       TRUE    base  
#> 3 Math    difftime   TRUE    base  
#> 4 Math    factor     TRUE    base  
#> 5 Math    POSIXt     TRUE    base

Reading source code for a few of the methods:

Math.factor() and Math.Date() provide only error message:

Math.factor <- function(x, ...) {
  stop(gettextf("%s not meaningful for factors", sQuote(.Generic)))
}

Math.Date <- function(x, ...) {
  stop(gettextf("%s not defined for \"Date\" objects", .Generic),
    domain = NA
  )
}

Math.data.frame() is defined as follows (except the first line of code, which I have deliberately added):

Math.data.frame <- function(x, ...) {
  message(paste0("Environment variable `.Generic` set to: ", .Generic))

  mode.ok <- vapply(x, function(x) {
    is.numeric(x) || is.logical(x) || is.complex(x)
  }, NA)

  if (all(mode.ok)) {
    x[] <- lapply(X = x, FUN = .Generic, ...)
    return(x)
  } else {
    vnames <- names(x)
    if (is.null(vnames)) vnames <- seq_along(x)
    stop(
      "non-numeric-alike variable(s) in data frame: ",
      paste(vnames[!mode.ok], collapse = ", ")
    )
  }
}

As can be surmised from the code: the method checks that all elements are of the same and expected type.

If so, it applies the generic (tracked via the environment variable .Generic) to each element of the list of atomic vectors that makes up a data frame:

df1 <- data.frame(x = 1:2, y = 3:4)
sqrt(df1)
#> Environment variable `.Generic` set to: sqrt
#>          x        y
#> 1 1.000000 1.732051
#> 2 1.414214 2.000000

If not, it produces an error:

df2 <- data.frame(x = c(TRUE, FALSE), y = c("a", "b"))
abs(df2)
#> Environment variable `.Generic` set to: abs
#> Error in Math.data.frame(df2): non-numeric-alike variable(s) in data frame: y

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

A3. Math.difftime() source code looks like the following:

Math.difftime <- function(x, ...) {
  switch(.Generic,
    "abs" = ,
    "sign" = ,
    "floor" = ,
    "ceiling" = ,
    "trunc" = ,
    "round" = ,
    "signif" = {
      units <- attr(x, "units")
      .difftime(NextMethod(), units)
    },
    ### otherwise :
    stop(gettextf("'%s' not defined for \"difftime\" objects", .Generic),
      domain = NA
    )
  )
}

This group generic is a bit more complicated because it produces an error for some generics, while it works for others.

13.7 Session information

sessioninfo::session_info(include_base = TRUE)
#> ─ Session info ───────────────────────────────────────────
#>  setting  value
#>  version  R version 4.4.0 (2024-04-24)
#>  os       Ubuntu 22.04.4 LTS
#>  system   x86_64, linux-gnu
#>  ui       X11
#>  language (EN)
#>  collate  C.UTF-8
#>  ctype    C.UTF-8
#>  tz       UTC
#>  date     2024-05-20
#>  pandoc   3.2 @ /opt/hostedtoolcache/pandoc/3.2/x64/ (via rmarkdown)
#> 
#> ─ Packages ───────────────────────────────────────────────
#>  package     * version date (UTC) lib source
#>  base        * 4.4.0   2024-05-06 [3] local
#>  bookdown      0.39    2024-04-15 [1] RSPM
#>  bslib         0.7.0   2024-03-29 [1] RSPM
#>  cachem        1.1.0   2024-05-16 [1] RSPM
#>  cli           3.6.2   2023-12-11 [1] RSPM
#>  codetools     0.2-20  2024-03-31 [3] CRAN (R 4.4.0)
#>  compiler      4.4.0   2024-05-06 [3] local
#>  crayon        1.5.2   2022-09-29 [1] RSPM
#>  datasets    * 4.4.0   2024-05-06 [3] local
#>  digest        0.6.35  2024-03-11 [1] RSPM
#>  downlit       0.4.3   2023-06-29 [1] RSPM
#>  dplyr       * 1.1.4   2023-11-17 [1] RSPM
#>  evaluate      0.23    2023-11-01 [1] RSPM
#>  fansi         1.0.6   2023-12-08 [1] RSPM
#>  fastmap       1.2.0   2024-05-15 [1] RSPM
#>  fs            1.6.4   2024-04-25 [1] RSPM
#>  generics      0.1.3   2022-07-05 [1] RSPM
#>  glue          1.7.0   2024-01-09 [1] RSPM
#>  graphics    * 4.4.0   2024-05-06 [3] local
#>  grDevices   * 4.4.0   2024-05-06 [3] local
#>  htmltools     0.5.8.1 2024-04-04 [1] RSPM
#>  jquerylib     0.1.4   2021-04-26 [1] RSPM
#>  jsonlite      1.8.8   2023-12-04 [1] RSPM
#>  knitr         1.46    2024-04-06 [1] RSPM
#>  lifecycle     1.0.4   2023-11-07 [1] RSPM
#>  magrittr    * 2.0.3   2022-03-30 [1] RSPM
#>  memoise       2.0.1   2021-11-26 [1] RSPM
#>  methods     * 4.4.0   2024-05-06 [3] local
#>  pillar        1.9.0   2023-03-22 [1] RSPM
#>  pkgconfig     2.0.3   2019-09-22 [1] RSPM
#>  purrr       * 1.0.2   2023-08-10 [1] RSPM
#>  R6            2.5.1   2021-08-19 [1] RSPM
#>  rlang         1.1.3   2024-01-10 [1] RSPM
#>  rmarkdown     2.27    2024-05-17 [1] RSPM
#>  sass          0.4.9   2024-03-15 [1] RSPM
#>  sessioninfo   1.2.2   2021-12-06 [1] RSPM
#>  sloop       * 1.0.1   2019-02-17 [1] RSPM
#>  stats       * 4.4.0   2024-05-06 [3] local
#>  tibble        3.2.1   2023-03-20 [1] RSPM
#>  tidyselect    1.2.1   2024-03-11 [1] RSPM
#>  tools         4.4.0   2024-05-06 [3] local
#>  utf8          1.2.4   2023-10-22 [1] RSPM
#>  utils       * 4.4.0   2024-05-06 [3] local
#>  vctrs         0.6.5   2023-12-01 [1] RSPM
#>  withr         3.0.0   2024-01-16 [1] RSPM
#>  xfun          0.44    2024-05-15 [1] RSPM
#>  xml2          1.3.6   2023-12-04 [1] RSPM
#>  yaml          2.3.8   2023-12-11 [1] RSPM
#> 
#>  [1] /home/runner/work/_temp/Library
#>  [2] /opt/R/4.4.0/lib/R/site-library
#>  [3] /opt/R/4.4.0/lib/R/library
#> 
#> ──────────────────────────────────────────────────────────