commit c3409027c8fdff42c9acff8ab26b2c67907d98bf
parent b71d07101c9b29942bac8aa7283d4e25e4dcd8a2
Author: Arthur Shaw <47256431+arthur-shaw@users.noreply.github.com>
Date:   Thu, 29 Sep 2022 20:26:57 -0400
Draft notes (#28)
Diffstat:
| M | 15_S4.Rmd |  |  | 320 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- | 
1 file changed, 315 insertions(+), 5 deletions(-)
diff --git a/15_S4.Rmd b/15_S4.Rmd
@@ -1,13 +1,323 @@
 # S4
 
-**Learning objectives:**
+## Introduction
 
-- THESE ARE NICE TO HAVE BUT NOT ABSOLUTELY NECESSARY
+Object consists of:
 
-## SLIDE 1
+- Slots. Like fields in R6.
+- Methods. Accessed through generics. Dispatched to particular methods.
 
-- ADD SLIDES AS SECTIONS (`##`).
-- TRY TO KEEP THEM RELATIVELY SLIDE-LIKE; THESE ARE NOTES, NOT THE BOOK ITSELF.
+Uses functions to define classes and their methods:
+
+- `setClass()`. Define class and its components. 
+- `setGenerics()`. Define generic functions. Used to dispatch.
+- `setMethods()`. Define methods
+
+## Basics overview
+
+### Set class
+
+Define the class:
+
+```{r}
+setClass("Person", 
+  slots = c(
+    name = "character", 
+    age = "numeric"
+  )
+)
+```
+
+Create an instance of the class
+
+```{r}
+john <- new("Person", name = "John Smith", age = NA_real_)
+```
+
+
+### Set generics
+
+Define generic functions for setting and getting the age slot
+
+```{r}
+# get the value
+setGeneric("age", function(x) standardGeneric("age"))
+# set the value
+setGeneric("age<-", function(x, value) standardGeneric("age<-"))
+```
+
+### Set methods
+
+Define methods for the generics:
+
+```{r}
+# get the value
+setMethod("age", "Person", function(x) x@age)
+# set the value
+setMethod("age<-", "Person", function(x, value) {
+  x@age <- value
+  x
+})
+
+# set the value
+age(john) <- 50
+# get the value
+age(john)
+```
+
+To give a flavor, there is only one method per slot. In more realistic cases, there might be several methods.
+
+## Details on defining the class
+
+### Inheritance
+
+```{r, eval=FALSE}
+setClass("Employee", 
+  contains = "Person", 
+  slots = c(
+    boss = "Person"
+  ),
+  prototype = list(
+    boss = new("Person")
+  )
+)
+```
+
+### Instantiation
+
+Create an instance of the class at two levels:
+
+- For developer (you): `methods::new()`
+- For user: constructor function
+
+```{r}
+# how user constructs an instance
+Person <- function(name, age = NA) {
+  age <- as.double(age)
+  
+  # how the developer constructs an instance
+  new("Person", name = name, age = age)
+}
+
+Person("Someone")
+```
+
+### Validation
+
+S4 objects
+
+- Check class of slot at creation
+```{r}
+Person(mtcars)
+```
+
+- Do **not** check other things
+```{r}
+Person("Hadley", age = c(30, 37))
+```
+
+That's where validation comes in--at two stages:
+
+1. At creation
+2. At modification
+
+#### At creation
+
+```{r}
+setValidity("Person", function(object) {
+  if (length(object@name) != length(object@age)) {
+    "@name and @age must be same length"
+  } else {
+    TRUE
+  }
+})
+
+Person("Hadley", age = c(30, 37))
+```
+
+#### At modification
+
+```{r}
+# get value
+setGeneric("name", function(x) standardGeneric("name"))
+setMethod("name", "Person", function(x) x@name)
+
+# set value--and assess whether resulting object is valid
+setGeneric("name<-", function(x, value) standardGeneric("name<-"))
+setMethod("name<-", "Person", function(x, value) {
+  x@name <- value
+  validObject(x)
+  x
+})
+
+# normal name; no problem
+name(john) <- "Jon Smythe"
+name(john)
+
+# invalid name; error thrown
+name(john) <- letters
+```
+
+
+## Details on generics and methods
+
+### Dictate dispatch via signature
+
+Specify function arguments to be used in determining method.
+
+```{r}
+setGeneric("myGeneric", 
+  function(x, ..., verbose = TRUE) standardGeneric("myGeneric"),
+  signature = "x"
+)
+```
+
+### Define generics
+
+General form:
+
+```{r, eval=FALSE}
+setMethod("myGeneric", "Person", function(x) {
+  # method implementation
+})
+```
+
+Example to print object:
+
+```{r}
+setMethod("show", "Person", function(object) {
+  cat(is(object)[[1]], "\n",
+      "  Name: ", object@name, "\n",
+      "  Age:  ", object@age, "\n",
+      sep = ""
+  )
+})
+john
+```
+
+Example to access slot:
+
+```{r}
+setGeneric("name", function(x) standardGeneric("name"))
+setMethod("name", "Person", function(x) x@name)
+
+name(john)
+```
+
+
+This is how end users should access slots.
+
+## Example: `lubridate::period()`
+
+### Define the class
+
+```{r, eval=FALSE}
+setClass("Period",
+  # inherits from these classes
+  contains = c("Timespan", "numeric"),
+  # has slots for time components
+  slots = c(
+    year = "numeric", 
+    month = "numeric", 
+    day = "numeric",
+    hour = "numeric", 
+    minute = "numeric"
+  ),
+  # defines prototype as period of zero duration for all slots
+  prototype = prototype(year = 0, month = 0, day = 0, hour = 0, minute = 0),
+  # check validity with `check_period` function; see section below
+  validity = check_period
+)
+```
+
+See source code [here](https://github.com/tidyverse/lubridate/blob/0bb49b21c88736240219dc67e7ed0eb3df15d9b1/R/periods.r#L90)
+
+### Validate object
+
+Check whether object is valid--notably if all arugments have the same length and are integers.
+
+```{r, eval=FALSE}
+check_period <- function(object) {
+  # start with an empty vector of error messages
+  errors <- character()
+
+  # check length of object's data
+  length(object@.Data) -> n
+  # check length of each slot
+  lengths <- c(
+    length(object@year), 
+    length(object@month),
+    length(object@day), 
+    length(object@hour), 
+    length(object@minute)
+  )
+
+  # if length of any slot is different than overall length, compose error message
+  if (any(lengths != n)) {
+    msg <- paste("Inconsistent lengths: year = ", lengths[1],
+      ", month = ", lengths[2],
+      ", day = ", lengths[3],
+      ", hour = ", lengths[4],
+      ", minute = ", lengths[5],
+      ", second = ", n,
+      sep = ""
+    )
+    # add just-composed error to vector of error messages
+    errors <- c(errors, msg)
+  }
+
+  values <- c(object@year, object@month, object@day, object@hour, object@minute)
+  values <- na.omit(values)
+  if (sum(values - trunc(values))) {
+    msg <- "periods must have integer values"
+    errors <- c(errors, msg)
+  }
+
+  if (length(errors) == 0) {
+    TRUE
+  } else {
+    errors
+  }
+}
+```
+
+
+See source code [here](https://github.com/tidyverse/lubridate/blob/0bb49b21c88736240219dc67e7ed0eb3df15d9b1/R/periods.r#L6).
+
+### Set methods
+
+Show period:
+
+```{r, eval=FALSE}
+#' @export
+setMethod("show", signature(object = "Period"), function(object) {
+  if (length(object@.Data) == 0) {
+    cat("<Period[0]>\n")
+  } else {
+    print(format(object))
+  }
+})
+
+#' @export
+format.Period <- function(x, ...) {
+  if (length(x) == 0) {
+    return(character())
+  }
+
+  show <- paste(
+    x@year, "y ", x@month, "m ", x@day, "d ",
+    x@hour, "H ", x@minute, "M ", x@.Data, "S",
+    sep = ""
+  )
+  start <- regexpr("[-1-9]|(0\\.)", show)
+  show <- ifelse(start > 0, substr(show, start, nchar(show)), "0S")
+
+  show[is.na(x)] <- NA
+  show
+}
+```
+
+See source code [here](https://github.com/tidyverse/lubridate/blob/0bb49b21c88736240219dc67e7ed0eb3df15d9b1/R/periods.r#L195)
 
 ## Meeting Videos