Learning Objectives

Motivation

S3 Basics

Classes

Constructor

  • Your constructor should

    • Be called new_myclass(), replacing “myclass” with the name of your class.
    • Have one argument for the base object (e.g. list, numeric vector, etc).
    • Check the type of the base object and types of each attribute.
  • E.g. if we were to create our own class to recapitulate factors, called factor2, we would do

    new_factor2 <- function(x = integer(), levels = character()) {
      stopifnot(is.integer(x))
      stopifnot(is.character(levels))
    
      return(
        structure(x,
                  levels = levels,
                  class = "factor2")
      )
    }
  • We can construct a factor as follows

    x <- new_factor2(c(1L, 1L, 2L, 1L, 1L), levels = c("A", "B"))
    x
    ## [1] 1 1 2 1 1
    ## attr(,"levels")
    ## [1] "A" "B"
    ## attr(,"class")
    ## [1] "factor2"

Validator

  • Making sure the structure of an object is what you would expect is expensive.

  • E.g., we need to make sure that the number of unique values in a factor is at most the number of levels in that factor.

  • Validator functions should:

    1. Be named validator_myclass().
    2. Take as input just an object from your class.
    3. Include a bunch of assertions testing the structure of the inputted object.
    4. Return the original object.
  • Let’s make a validator for factor2.

    validate_factor2 <- function(x) {
      stopifnot(inherits(x, "factor2"))
      values <- unclass(x)
      levels <- attr(x, "levels")
    
      if (length(levels) < max(values)) {
        stop("There must be at least as many `levels` as possible values in `x`")
      }
      return(x)
    }
    validate_factor2(x)
    ## [1] 1 1 2 1 1
    ## attr(,"levels")
    ## [1] "A" "B"
    ## attr(,"class")
    ## [1] "factor2"

Helpers

  • A helper function is a user-facing function that will

    1. Be called myclass().
    2. Call first the constructor function, then the validator function.
    3. Be user friendly.
      • Good defaults.
      • Accepts multiple types for the base object and coerces intelligently.
  • Let’s do this for factor2.

    factor2 <- function(x = character(), levels = unique(x)) {
      ind <- match(x, levels)
      return(validate_factor2(new_factor2(ind, levels)))
    }
    
    factor2(c("A", "B", "B", "A"))
    ## [1] 1 2 2 1
    ## attr(,"levels")
    ## [1] "A" "B"
    ## attr(,"class")
    ## [1] "factor2"
  • Side note: match() is a useful function. It will provide the positions of the second argument that match the values in the second argument. E.g.

    match(c("A", "A", "B", "A", "B"), c("A", "B"))
    ## [1] 1 1 2 1 2
  • Exercise (Advanced R): 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?

Generics

Methods

The Design of an S3 Object

  1. A list-like object, where the list represents one thing (e.g. model output, function, dataset, etc…).

    • For example, the output of lm() is a list like object that represents one model fit.
    lmout <- lm(mpg ~ wt, data = mtcars)
    sloop::otype(lmout)
    ## [1] "S3"
    typeof(lmout)
    ## [1] "list"
    • I use this format all of the time for the outputs of my model fits.
  2. A vector with new functionality. E.g. factors and Dates. You combine, print, mathematically operate with these vectors in different ways.

    x <- factor(c("A", "A", "B", "A", "B"))
    sloop::otype(x)
    ## [1] "S3"
    typeof(x)
    ## [1] "integer"
  3. Lists of equal length length vectors. E.g. data.frames and POSIXlt objects.

    • POSIXlt objects are lists of years, days, minutes, seconds, etc… with the ith element of each vector contributing to indicating the same moment in time.
    x <- as.POSIXlt(ISOdatetime(2020, 1, 1, 0, 0, 1:3))
    x
    ## [1] "2020-01-01 00:00:01 EST" "2020-01-01 00:00:02 EST"
    ## [3] "2020-01-01 00:00:03 EST"
    typeof(x)
    ## [1] "list"
    • data.frame objects are lists of vectors where each vector is a variable and the ith element of each vector represents the same observational unit.
    typeof(mtcars)
    ## [1] "list"

Inheritance

Next Method

  • NextMethod() allows you define methods for your class that use the functionality of classes that you inherit from.

  • E.g. recall that most attributes are lost with [.

    x <- factor2(c("A", "A", "B", "A", "B"))
    x[1]
    ## [1] 1
  • This is because R is using the integer version for [ and so we lose the class.

    sloop::s3_dispatch(x[1])
    ##    [.factor2
    ##    [.default
    ## => [ (internal)
  • We cannot use [ inside a definition for a method because we haven’t defined it yet.

    ## won't work
    `[.factor2` <- function(x, i) {
      return(x[i]) # but we haven't defined `[` yet
    }
    x[1] ## infinite recursion
  • You can define your method to use the next method by NextMethod(). NextMethod() will take the arguments inside your function definition and run them through the next method in the inheritance list. So it returns an unclassed object, that you can then pass to your constructor function.

  • Make sure you also include the attributes in your constructor.

    `[.factor2` <- function(x, i) {
      new_factor2(NextMethod(), levels = attr(x, "levels"))
    }
    sloop::s3_dispatch(x[1:3])
    ## => [.factor2
    ##    [.default
    ## -> [ (internal)
    x[1:3]
    ## [1] "A" "A" "B"
  • If we did not pass NextMethod() to our constructor, it would just run the integer subsetting:

    `[.factor2` <- function(x, i) {
      NextMethod()
    }
    x[1:3]
    ## [1] 1 1 2

Example: Simulation

Documenting S3

Type Predicates

Method Dispatch Technicalities

{vctrs} package

New functions


National Science Foundation Logo American University Logo Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.