S3

In this lab, we will learn how to write methods for S3 generics.

Goal: by the end of this lab, you should be able to write your own S3 generic methods and functions.

Finding out about S3 methods

First, we’ll load the sloop package to get some information about the S3 system.

library(sloop)

Since we’ve already loaded the tidyverse, there are lots of S3 generic function and their methods already available to us.

For example, there are many methods for the print() generic available:

s3_methods_generic("print")
# A tibble: 303 × 4
   generic class             visible source             
   <chr>   <chr>             <lgl>   <chr>              
 1 print   acf               FALSE   registered S3method
 2 print   activeConcordance FALSE   registered S3method
 3 print   AES               FALSE   registered S3method
 4 print   all_vars          FALSE   registered S3method
 5 print   anova             FALSE   registered S3method
 6 print   any_vars          FALSE   registered S3method
 7 print   aov               FALSE   registered S3method
 8 print   aovlist           FALSE   registered S3method
 9 print   ar                FALSE   registered S3method
10 print   Arima             FALSE   registered S3method
# ℹ 293 more rows

In fact, there are so many that we might as well restrict our attention to those relevant to tibbles and data.frames.

s3_methods_generic("print") |>
  filter(str_detect(class, "tbl|data.frame"))
# A tibble: 3 × 4
  generic class                   visible source             
  <chr>   <chr>                   <lgl>   <chr>              
1 print   data.frame              TRUE    base               
2 print   pillar_tbl_format_setup FALSE   registered S3method
3 print   tbl                     FALSE   registered S3method

Other common S3 generics include summary() and plot():

s3_methods_generic("summary")
# A tibble: 44 × 4
   generic class                 visible source             
   <chr>   <chr>                 <lgl>   <chr>              
 1 summary aov                   TRUE    stats              
 2 summary aovlist               FALSE   registered S3method
 3 summary aspell                FALSE   registered S3method
 4 summary check_packages_in_dir FALSE   registered S3method
 5 summary connection            TRUE    base               
 6 summary data.frame            TRUE    base               
 7 summary Date                  TRUE    base               
 8 summary default               TRUE    base               
 9 summary Duration              FALSE   registered S3method
10 summary ecdf                  FALSE   registered S3method
# ℹ 34 more rows
s3_methods_generic("plot")
# A tibble: 35 × 4
   generic class         visible source             
   <chr>   <chr>         <lgl>   <chr>              
 1 plot    acf           FALSE   registered S3method
 2 plot    data.frame    FALSE   registered S3method
 3 plot    decomposed.ts FALSE   registered S3method
 4 plot    default       TRUE    graphics           
 5 plot    dendrogram    FALSE   registered S3method
 6 plot    density       FALSE   registered S3method
 7 plot    ecdf          TRUE    stats              
 8 plot    factor        FALSE   registered S3method
 9 plot    formula       FALSE   registered S3method
10 plot    function      TRUE    graphics           
# ℹ 25 more rows
  1. Try to think of another generic function that you’ve worked with in the past. Use s3_methods_generic() to find its available methods. [Note that you might have to load other packages to find the methods or the generic that you recall.]

In addition to listing the methods for a particular generic, we can find all the methods that are defined for a particular class.

s3_methods_class("tbl_df")
# A tibble: 21 × 4
   generic       class  visible source             
   <chr>         <chr>  <lgl>   <chr>              
 1 [             tbl_df FALSE   registered S3method
 2 [[            tbl_df FALSE   registered S3method
 3 [[<-          tbl_df FALSE   registered S3method
 4 [<-           tbl_df FALSE   registered S3method
 5 $             tbl_df FALSE   registered S3method
 6 $<-           tbl_df FALSE   registered S3method
 7 arrange_      tbl_df FALSE   registered S3method
 8 as.data.frame tbl_df FALSE   registered S3method
 9 distinct_     tbl_df FALSE   registered S3method
10 filter_       tbl_df FALSE   registered S3method
# ℹ 11 more rows

Note that while the methods for a particular generic are most likely written by many different developers in many different packages, the methods for a particular class are most likely written by the same set of developers of a single package.

Method dispath

If you are ever unsure of which method is actually being run when you invoke a generic, use s3_dispatch() to figure out what is going on.

s3_dispatch(print(starwars))
   print.tbl_df
=> print.tbl
 * print.data.frame
 * print.default
s3_dispatch(print(mtcars))
=> print.data.frame
 * print.default
  1. Why do the previous two examples show a different list of available methods?

  2. Why doesn’t print.data.frame() get called on starwars?

Defining a method

First, we’ll learn about two fun packages: emoji and crayon.

emoji

The emoji package allows you print emoji via the emoji() function.

install.packages("emoji")

To display an emoji, call it by name:

library(emoji)
emoji("star")
[1] "⭐"
emoji("dog")
[1] "🐶"
emoji("basketball")
[1] "🏀"

You can see the full list of available emojis in the tibble returned by emojis.

emojis |> View()
  1. Find an emoji that you like and use emoji() to print it to the console.

crayon

The crayon package allows you to print in color in the console. For a complicated example of what you can do, consider the tidyverse logo:

tidyverse_logo()
⬢ __  _    __   .    ⬡           ⬢  . 
 / /_(_)__/ /_ ___  _____ _______ ___ 
/ __/ / _  / // / |/ / -_) __(_-</ -_)
\__/_/\_,_/\_, /|___/\__/_/ /___/\__/ 
     ⬢  . /___/      ⬡      .       ⬢ 

For a simpler, example, you can write your name in blue. Be sure to wrap the text in cat() (or message()) to send it to the console.

cat(crayon::blue("Ben"))
Ben
  1. Pick a color from the crayon package and write something to the console in that color.

Defining a method

Now we will define a simple print() method that will only be invoked for objects of class beanumber, which is my GitHub username. This method will simply print a colorful message to the console, and then invoke the NextMethod() available.

print.beanumber <- function(x, ...) {
  cat(
    crayon::magenta(
      "Ben", 
      emoji::emoji("heart"), 
      "s programming in R!\n"
    )
  )
  NextMethod()
}

To use it, we just have to create an object of type beanumber, and then call print(). Remember that in R you can simply modify the class attribute of any object!

class(starwars) <- c("beanumber", class(starwars))
starwars
Ben ❤️ s programming in R!
# A tibble: 87 × 14
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 Luke Sk…    172    77 blond      fair       blue            19   male  mascu…
 2 C-3PO       167    75 <NA>       gold       yellow         112   none  mascu…
 3 R2-D2        96    32 <NA>       white, bl… red             33   none  mascu…
 4 Darth V…    202   136 none       white      yellow          41.9 male  mascu…
 5 Leia Or…    150    49 brown      light      brown           19   fema… femin…
 6 Owen La…    178   120 brown, gr… light      blue            52   male  mascu…
 7 Beru Wh…    165    75 brown      light      blue            47   fema… femin…
 8 R5-D4        97    32 <NA>       white, red red             NA   none  mascu…
 9 Biggs D…    183    84 black      light      brown           24   male  mascu…
10 Obi-Wan…    182    77 auburn, w… fair       blue-gray       57   male  mascu…
# ℹ 77 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Note that this method works on objects of any underlying class!!

class(Titanic)
[1] "table"
class(Titanic) <- c("beanumber", class(Titanic))
Titanic
Ben ❤️ s programming in R!
, , Age = Child, Survived = No

      Sex
Class  Male Female
  1st     0      0
  2nd     0      0
  3rd    35     17
  Crew    0      0

, , Age = Adult, Survived = No

      Sex
Class  Male Female
  1st   118      4
  2nd   154     13
  3rd   387     89
  Crew  670      3

, , Age = Child, Survived = Yes

      Sex
Class  Male Female
  1st     5      1
  2nd    11     13
  3rd    13     14
  Crew    0      0

, , Age = Adult, Survived = Yes

      Sex
Class  Male Female
  1st    57    140
  2nd    14     80
  3rd    75     76
  Crew  192     20
  1. Define your own print() method for objects having class [your GitHub username].

Defining a new generic

In the previous section, we defined a method for an existing generic function. Now, we will define a new generic function.

print_emoji <- function(x, ...) {
  UseMethod("print_emoji")
}

Since we haven’t defined any methods yet, the function won’t actually work!

print_emoji(starwars)
Error in UseMethod("print_emoji"): no applicable method for 'print_emoji' applied to an object of class "c('beanumber', 'tbl_df', 'tbl', 'data.frame')"

However, we can start by writing a default method that will simply call the existing print() generic.

print_emoji.default <- function(x, ...) {
  print(x, ...)
}

Now our print_emoji() function will work.

print_emoji(starwars)
Ben ❤️ s programming in R!
# A tibble: 87 × 14
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 Luke Sk…    172    77 blond      fair       blue            19   male  mascu…
 2 C-3PO       167    75 <NA>       gold       yellow         112   none  mascu…
 3 R2-D2        96    32 <NA>       white, bl… red             33   none  mascu…
 4 Darth V…    202   136 none       white      yellow          41.9 male  mascu…
 5 Leia Or…    150    49 brown      light      brown           19   fema… femin…
 6 Owen La…    178   120 brown, gr… light      blue            52   male  mascu…
 7 Beru Wh…    165    75 brown      light      blue            47   fema… femin…
 8 R5-D4        97    32 <NA>       white, red red             NA   none  mascu…
 9 Biggs D…    183    84 black      light      brown           24   male  mascu…
10 Obi-Wan…    182    77 auburn, w… fair       blue-gray       57   male  mascu…
# ℹ 77 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>
  1. In the default method for print_emoji(), can we use NextMethod() instead of print()? Why or why not?

  2. Write a print_emoji() method for data.frames.

Engagement

Prompt: Paste the code from your print_emoji() method to the #emojis channel. What does it do?