+ "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:::",
0 commit comments