Skip to content

Commit 51811c6

Browse files
authored
S7 (#105)
* Add an introduction to S7 as chapter 15b I also updated the meeting videos through chapter 15 (except not 14 yet, since we're out of order). * Add slides for S7
1 parent 8646774 commit 51811c6

File tree

21 files changed

+779
-47
lines changed

21 files changed

+779
-47
lines changed

_freeze/slides/13/execute-results/html.json

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

_freeze/slides/15/execute-results/html.json

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"hash": "a6add3291377f31481951873fb22e1ce",
3+
"result": {
4+
"engine": "knitr",
5+
"markdown": "---\nengine: knitr\ntitle: S7\n---\n\n## Learning objectives\n\n- Recognize the challenges with S3 and S4 that motivated development of S7\n- Create S7 classes\n- Create S7 generics and methods\n- Define custom constructors for S7 classes\n- Use S7 with S3 and/or S4\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(S7)\n```\n:::\n\n\n# Motivation for S7\n\n## S7 resolves challenges with S3\n\n- S3 has no formal class definitions\n- S3 methods are difficult to find\n- S3 properties are in attributes, but `attr()` does fuzzy matching\n- S3 method dispatch via `UseMethod()` is fuzzy\n\n> \"Now for some obscure details that need to appear somewhere\" —`?UseMethod`\n\n- `NextMethod()` depends on what's loaded\n- Conversion between S3 classes is fuzzy (*vs* `S7::convert()` generic)\n\n## S7 resolves challenges with S4\n\n- S4's multiple inheritance causes more problems than it solves\n- S4's method dispatch is smart but hard to predict (S7 is explicit)\n- S4 is clean break from S3, made it hard to switch from S3 to S4\n- S4 pretends users can't use `@` to access slots, but they do it anyway\n\n## S7 extends S3 and replaces S4\n\n- Name comes from S3 + S4 = S7, but...\n - S7 objects *are* S3 objects\n - S7 objects *are not* S4 objects\n - But there's some overlap (see last section)\n\n# Classes and objects (and properties)\n\n::: notes\nThis section combines the [1st section of S7 basics](https://rconsortium.github.io/S7/articles/S7.html#classes-and-objects) with the [Classes and objects vignette](https://rconsortium.github.io/S7/articles/classes-objects.html) \n:::\n\n## Define S7 classes with `S7::new_class()`\n\n\n::: {.cell}\n\n```{.r .cell-code}\nclass_person <- new_class(\n \"Person\",\n properties = list(name = class_character, age = class_numeric)\n)\nme <- class_person(name = \"Jon\", age = 50)\nme\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> <Person>\n#> @ name: chr \"Jon\"\n#> @ age : num 50\n```\n\n\n:::\n\n```{.r .cell-code}\nS7_inherits(me, class_person)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] TRUE\n```\n\n\n:::\n\n```{.r .cell-code}\nclass(me)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] \"Person\" \"S7_object\"\n```\n\n\n:::\n\n```{.r .cell-code}\ninherits(me, \"Person\")\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] TRUE\n```\n\n\n:::\n:::\n\n\n::: notes\n- Vignette shows same name for the class object and the 1st arg, but in practice I find it safer to differentiate them (in case you use the class as a property of another class).\n - Notice that you use `class_character`, not `character`; I think that's connected to the issues I had with name matching\n- Equivalent to constructor + validator + helper from S3 chapter\n - We'll see this more explicitly later\n- Equivalent to `setClass()` + `new()` + `setValidity()` from S4 chapter\n- S3 methods will dispatch properly for this object.\n:::\n\n## Access S7 object properties with `@`\n\n\n::: {.cell}\n\n```{.r .cell-code}\nme@name\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] \"Jon\"\n```\n\n\n:::\n\n```{.r .cell-code}\nme@age\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] 50\n```\n\n\n:::\n:::\n\n\n::: notes\n- Like S3 attributes, but `attr()` is fuzzy, and S3 attributes are less formally defined.\n- Like S4 slots, but S4 users are discouraged from using `@` directly, and S4 `@` doesn't trigger validation (see next).\n:::\n\n## S7 objects are validated during construction and on property assignment\n\n\n::: {.cell}\n\n```{.r .cell-code}\nme@age <- \"fifty\"\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error: <Person>@age must be <integer> or <double>, not <character>\n```\n\n\n:::\n\n```{.r .cell-code}\nus <- class_person(name = 1:2, age = c(\"fifty\", \"forty-nine\"))\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error: <Person> object properties are invalid:\n#> - @name must be <character>, not <integer>\n#> - @age must be <integer> or <double>, not <character>\n```\n\n\n:::\n:::\n\n\n## `validator` argument customizes validation\n\n\n::: {.cell}\n\n```{.r .cell-code}\nclass_person <- new_class(\n \"Person\",\n properties = list(name = class_character, age = class_numeric),\n validator = function(self) {\n if (length(self@name) != length(self@age)) {\n \"@name and @age must be the same length\"\n }\n }\n)\nus <- class_person(name = c(\"Jon\", \"Leyla\"), age = c(50, -5, 49))\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error: <Person> object is invalid:\n#> - @name and @age must be the same length\n```\n\n\n:::\n\n```{.r .cell-code}\nme <- class_person(\"Jon\", 50)\nme@age <- 50:60\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error: <Person> object is invalid:\n#> - @name and @age must be the same length\n```\n\n\n:::\n:::\n\n\n::: notes\nCan return more than one string.\n:::\n\n## Set properties all at once to avoid intermediate invalid states\n\n\n::: {.cell}\n\n```{.r .cell-code}\nus <- me\nus@name <- c(\"Jon\", \"Leyla\")\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error: <Person> object is invalid:\n#> - @name and @age must be the same length\n```\n\n\n:::\n\n```{.r .cell-code}\n# Quick S3 method to demonstrate\nc.Person <- function(x, y) {\n props(x) <- list(name = c(x@name, y@name), age = c(x@age, y@age))\n x\n}\nus <- c(me, class_person(\"Leyla\", 49))\nus\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> <Person>\n#> @ name: chr [1:2] \"Jon\" \"Leyla\"\n#> @ age : num [1:2] 50 49\n```\n\n\n:::\n:::\n\n\n::: notes\nThis is also a preview of why S7 is easier to adopt in an S3 world\n:::\n\n## `S7::new_property()` defines custom property types\n\n\n::: {.cell}\n\n```{.r .cell-code}\nprop_positive <- new_property(\n class = class_numeric,\n validator = function(value) {\n if (any(value <= 0)) \"must be positive\"\n }\n)\nclass_person <- new_class(\n \"Person\",\n properties = list(name = class_character, age = prop_positive),\n validator = function(self) {\n if (length(self@name) != length(self@age)) {\n \"@name and @age must be the same length\"\n }\n }\n)\nus <- class_person(name = c(\"Jon\", \"Leyla\"), age = c(50, -5))\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error: <Person> object properties are invalid:\n#> - @age must be positive\n```\n\n\n:::\n:::\n\n\n::: notes\nCan also use this to set default values\n:::\n\n## Properties can be computed\n\n\n::: {.cell}\n\n```{.r .cell-code}\nclass_circle <- new_class(\n \"Circle\",\n properties = list(\n radius = class_numeric,\n area = new_property(\n class = class_numeric,\n getter = function(self) {\n pi * self@radius^2\n }\n )\n )\n)\nc1 <- class_circle(radius = 1)\nc1@area == pi\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] TRUE\n```\n\n\n:::\n\n```{.r .cell-code}\nc1@radius <- 2\nc1@area == 4*pi\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] TRUE\n```\n\n\n:::\n:::\n\n\n## Properties can be fully dynamic\n\n\n::: {.cell}\n\n```{.r .cell-code}\nclass_circle2 <- new_class(\n \"Circle\",\n properties = list(\n radius = class_numeric,\n area = new_property(\n class = class_numeric,\n getter = function(self) pi * self@radius^2,\n setter = function(self, value) {\n if (!length(value)) return(self)\n self@radius <- sqrt(value / pi)\n self\n }\n )\n )\n)\nc2 <- class_circle2(radius = 1)\nc2@area\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] 3.141593\n```\n\n\n:::\n\n```{.r .cell-code}\nc2@area <- 4*pi\nc2@radius\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] 2\n```\n\n\n:::\n\n```{.r .cell-code}\nc2@radius <- 3\nc2@area == 9*pi\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] TRUE\n```\n\n\n:::\n:::\n\n\n::: notes\n- This starts to feel kinda encapsulated like we'll see in R6\n- Still works with S3-style multimethods, though\n:::\n\n## `constructor` argument customizes object creation\n\n- Not going into detail yet.\n\n## `parent` argument specifies the parent\n\n- Not going into detail yet.\n\n# Generics and methods\n\n::: notes\nThis section combines the [2nd and 3rd section of S7 basics](https://rconsortium.github.io/S7/articles/S7.html#generics-and-methods) with the [Generics and methods vignette](https://rconsortium.github.io/S7/articles/generics-methods.html#generic-method-compatibility) \n:::\n\n## Define S7 generics & methods with `S7::new_generic()` and `S7::new_method()`\n\n\n::: {.cell}\n\n```{.r .cell-code}\nare_old <- new_generic(\"are_old\", \"x\")\nmethod(are_old, class_person) <- function(x) {\n x@age >= 40 & x@name != \"Leyla\"\n}\nus\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> <Person>\n#> @ name: chr [1:2] \"Jon\" \"Leyla\"\n#> @ age : num [1:2] 50 49\n```\n\n\n:::\n\n```{.r .cell-code}\nare_old(us)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] TRUE FALSE\n```\n\n\n:::\n:::\n\n\n## S7 method dispatch is similar to S3\n\n\n::: {.cell}\n\n```{.r .cell-code}\nclass_geek <- new_class(\n \"Geek\",\n parent = class_person\n)\nme <- class_geek(\"Jon\", 50)\nare_old(me) # Dispatches on \"Person\"\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] TRUE\n```\n\n\n:::\n:::\n\n\n## S7 method chaining is explicit\n\n\n::: {.cell}\n\n```{.r .cell-code}\nmethod(are_old, class_geek) <- function(x) {\n cat(\"Checking if geeks are old...\\n\")\n are_old(super(x, class_person)) # Must specify which parent class to use\n}\nare_old(me)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> Checking if geeks are old...\n```\n\n\n:::\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] TRUE\n```\n\n\n:::\n:::\n\n\n## S7 generics allow for multiple dispatch\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncombine <- new_generic(\"combine\", c(\"x\", \"y\"))\nmethod(combine, list(class_person, class_person)) <- function(x, y) {\n class_person(\n name = c(x@name, y@name),\n age = c(x@age, y@age)\n )\n}\nhw <- class_geek(\"Hadley\", 46)\ncombine(us, hw)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> <Person>\n#> @ name: chr [1:3] \"Jon\" \"Leyla\" \"Hadley\"\n#> @ age : num [1:3] 50 49 46\n```\n\n\n:::\n\n```{.r .cell-code}\ncombine(me, hw)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> <Person>\n#> @ name: chr [1:2] \"Jon\" \"Hadley\"\n#> @ age : num [1:2] 50 46\n```\n\n\n:::\n\n```{.r .cell-code}\nmethod(combine, list(class_geek, class_geek)) <- function(x, y) {\n combined_person <- combine(\n super(x, class_person),\n super(y, class_person)\n )\n class_geek(\n name = combined_person@name,\n age = combined_person@age\n )\n}\ncombine(me, hw)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> <Geek>\n#> @ name: chr [1:2] \"Jon\" \"Hadley\"\n#> @ age : num [1:2] 50 46\n```\n\n\n:::\n:::\n\n\n- `class_any` matches any class\n- `class_missing` for missing arguments\n\n# Compatibility\n\n## Compatibility with S3\n\n- `class()` for S3 classes, `S7_class()` for S7 class constructor\n- S7 properties are attributes (so old code that expects those will work)\n- S7 can register methods for:\n - S7 class + S3 generic\n - S3 class + S7 generic\n- S7 classes can inherit from S3 classes\n- S3 classes can inherit from S7 classes\n\n## Compatibility with S4\n\n- S7 classes cannot inherit from S4 classes\n- S4 classes can inherit from S7 classes\n- S7 can register methods for:\n - S7 class + S4 generic\n - S4 class + S7 generic\n- Out of scope: Both support class unions\n\n::: notes\n- I didn't try any S4 + S7 code, and there's a typo (I think) in that second bullet on the site (it says \"S4 classes can inherit from S3 classes\")\n:::",
6+
"supporting": [
7+
"15b_files"
8+
],
9+
"filters": [
10+
"rmarkdown/pagebreak.lua"
11+
],
12+
"includes": {
13+
"include-after-body": [
14+
"\n<script>\n // htmlwidgets need to know to resize themselves when slides are shown/hidden.\n // Fire the \"slideenter\" event (handled by htmlwidgets.js) when the current\n // slide changes (different for each slide format).\n (function () {\n // dispatch for htmlwidgets\n function fireSlideEnter() {\n const event = window.document.createEvent(\"Event\");\n event.initEvent(\"slideenter\", true, true);\n window.document.dispatchEvent(event);\n }\n\n function fireSlideChanged(previousSlide, currentSlide) {\n fireSlideEnter();\n\n // dispatch for shiny\n if (window.jQuery) {\n if (previousSlide) {\n window.jQuery(previousSlide).trigger(\"hidden\");\n }\n if (currentSlide) {\n window.jQuery(currentSlide).trigger(\"shown\");\n }\n }\n }\n\n // hookup for slidy\n if (window.w3c_slidy) {\n window.w3c_slidy.add_observer(function (slide_num) {\n // slide_num starts at position 1\n fireSlideChanged(null, w3c_slidy.slides[slide_num - 1]);\n });\n }\n\n })();\n</script>\n\n"
15+
]
16+
},
17+
"engineDependencies": {},
18+
"preserve": {},
19+
"postProcess": true
20+
}
21+
}

_quarto.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ website:
5555
- 12.qmd
5656
- 13.qmd
5757
- 15.qmd
58+
- 15b.qmd
5859
- 14.qmd
5960
- 16.qmd
6061
- section: Metaprogramming

slides/13.qmd

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ sloop::s3_methods_generic(
162162
knitr::kable()
163163
```
164164

165+
::: notes
166+
This slide throws a warning because the generated table actually includes 3 colons. It isn't a "problemwith a fenced div" like the warning thinks.
167+
:::
168+
165169
## `summary()` is an interface to different methods
166170

167171
- Polymorphism: a single interface to different behaviors.
@@ -445,4 +449,4 @@ vec_restore.supersecret <- function(x, to, ...) new_supersecret(x)
445449
}
446450
x2[1:3]
447451
```
448-
:::
452+
:::

slides/15.qmd

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ engine: knitr
33
title: S4
44
---
55

6-
## Learning Objectives
6+
## Learning objectives
77

88
- Identify the main components of S4 objects, including the new **slot** component
99
- Learn best practices for creating new S4 classes and creating/modifying/accessing their objects
1010
- "Understand" multiple inheritance, multiple dispatch, and appreciate their risks and complexity
1111

12-
## Not Learning Objectives
12+
## Not learning objectives
1313

1414
- How to most effectively deploy S4
1515
- No single reference.
@@ -355,7 +355,7 @@ Keep method dispatch as simple as possible by avoiding multiple inheritance, and
355355

356356
Class with the shortest distance to the specified class gets dispatched.
357357

358-
## Multiple inheritance can lead to ambigious method dispatch
358+
## Multiple inheritance can lead to ambiguous method dispatch
359359

360360
::::{.columns}
361361
:::{.column}
@@ -421,7 +421,7 @@ setOldClass("factor", S4Class = "factor")
421421
These definitions should be provided by the creator of the S3 class. Don't trying building an S4 class on top of an S3 class provided by a package. Instead request that the package maintainer add this call to their package.
422422
:::
423423

424-
## S4 classes recieve as special `.Data` slot when inheriting from S3 or base types
424+
## S4 classes receive as special `.Data` slot when inheriting from S3 or base types
425425

426426
```{r}
427427
RangedNumeric <- setClass(

0 commit comments

Comments
 (0)