class: left, middle, title-slide # Advanced R ## Chapter 14 - R6 ### Tyler Grant Smith ###
TylerGrantSmith
TylerGrantS
### 2020-07-26 --- <style> .hljs-github .hljs { color: #333; background: #f8f8f8 } </style> # R6 <div class='container'> <b>Package</b>: R6<br><b>Title</b>: Encapsulated Classes with Reference Semantics<br><b>Version</b>: 2.4.1<br><b>Authors@R</b>: person("Winston", "Chang", role = c("aut", "cre"), email = "winston@stdout.org")<br><b>Description</b>: Creates classes with reference semantics, similar to R's built-in reference classes. Compared to reference classes, R6 classes are simpler and lighter-weight, and they are not built on S4 classes so they do not require the methods package. These classes allow public and private members, and they support inheritance, even when the classes are defined in different packages.<br><b>Depends</b>: R (>= 3.0)<br><b>Suggests</b>: knitr, microbenchmark, pryr, testthat, ggplot2, scales<br><b>License</b>: MIT + file LICENSE<br><b>URL</b>: https://r6.r-lib.org, https://github.com/r-lib/R6/<br><b>LazyData</b>: true<br><b>BugReports</b>: https://github.com/r-lib/R6/issues<br><b>RoxygenNote</b>: 6.1.1<br><b>NeedsCompilation</b>: no<br><b>Packaged</b>: 2019-11-12 20:00:15 UTC; winston<br><b>Author</b>: Winston Chang [aut, cre]<br><b>Maintainer</b>: Winston Chang <winston@stdout.org><br><b>Repository</b>: CRAN<br><b>Date/Publication</b>: 2019-11-12 22:50:03 UTC<br><b>Built</b>: R 4.0.2; ; 2020-07-21 14:28:09 UTC; windows </div> --- # R6 in the wild #### How many? ```r depends <- attr(pkgsearch::advanced_search(Depends = "R6"), "metadata")$total imports <- attr(pkgsearch::advanced_search(Imports = "R6"), "metadata")$total suggests <- attr(pkgsearch::advanced_search(Suggests = "R6"), "metadata")$total glue("{depends + imports + suggests} packages on CRAN utilize `R6`.") ``` ``` ## 321 packages on CRAN utilize `R6`. ``` #### What are they? ```r pkgsearch::advanced_search(Imports = "R6", size = 300) %>% arrange(-downloads_last_month) %>% head(20) %>% pull("package") %>% paste0(collapse = ", ") %>% strwrap(width = 100) %>% cli::cat_line() ``` ``` ## dplyr, processx, pkgbuild, testthat, callr, vdiffr, desc, httr, scales, promises, selectr, httpuv, ## readr, shiny, progress, dbplyr, roxygen2, crosstalk, rcmdcheck, data.tree ``` --- class: show-only-last-code-result # R6 in the wild ```r "testthat" %>% getNamespace() %>% eapply(is.R6Class) %>% flatten_lgl() %>% which() %>% names() ``` ``` ## [1] "Stack" "TeamcityReporter" "RstudioReporter" "StopReporter" ## [5] "MultiReporter" "SilentReporter" "Reporter" "TapReporter" ## [9] "MinimalReporter" "CheckReporter" "FailReporter" "LocationReporter" ## [13] "ProgressReporter" "JunitReporter" "ListReporter" "SummaryReporter" ## [17] "DebugReporter" ``` -- ```r testthat:::Stack ``` ``` ## <Stack> object generator ## Public: ## initialize: function (init = 20L) ## push: function (..., .list = NULL) ## size: function () ## as_list: function () ## clone: function (deep = FALSE) ## Private: ## stack: NULL ## count: 0 ## init: 20 ## Parent env: <environment: namespace:testthat> ## Locked objects: TRUE ## Locked class: FALSE ## Portable: TRUE ``` --- class: hide-logo, capsule-slide # Encapsulation - In `S3` methods belong to the `generic`. - In `R6` methods belong to the object itself. --- # Reference Semantics - Value semantics leads to copy on modification - Reference semantics leads to reference change on modification .pull-left[ ### Value Semanics ```r x <- 1 y <- x x <- 2 y ``` ``` ## [1] 1 ``` ] .pull-right[ ### Reference Semantics ```r x <- new.env() y <- x x$a <- 2 y$a ``` ``` ## [1] 2 ``` ] - --- # Generating a 🍺 class The simplest `R6` class: ```r R6::R6Class() ``` ``` *## <unnamed> object generator ## Public: ## clone: function (deep = FALSE) ## Parent env: <environment: R_GlobalEnv> ## Locked objects: TRUE ## Locked class: FALSE ## Portable: TRUE ``` -- You should at least provide the first argument, `classname`: ```r Beer <- R6::R6Class("Beer") ``` -- .pull-left[ ```r class(Beer) ``` ``` ## [1] "R6ClassGenerator" ``` .center[**I (kinda) Lied**] ] -- .pull-right[ ```r typeof(Beer) ``` ``` ## [1] "environment" ``` .center[**Wait...what?**] ] --- # Instantiating a class ```r Beer <- R6::R6Class("Beer") ``` Instantiate your class using the generator's `new` method ```r beer <- Beer$new() beer ``` ``` ## <Beer> ## Public: ## clone: function (deep = FALSE) ``` We'll talk about cloning later, but you can disable it by passing `cloneable = FALSE` when defining the class ```r Beer <- R6::R6Class("Beer", cloneable = FALSE) Beer$new() ``` ``` ## <Beer> ## Public: ## : ``` --- # `public` The `public` argument to `R6::R6Class` accepts a named list of methods and/or objects. ```r Beer <- R6::R6Class("Beer", public = list(abv = .05)) ``` .pull-left[ #### External Access ```r beer <- Beer$new() *beer$abv ``` ``` ## [1] 0.05 ``` ] .pull-right[ #### Internal access ```r Beer <- R6::R6Class( "Beer", public = list( abv = 0.05, percent_abv = function() * sprintf("%.1f%%", 100 * self$abv) )) beer <- Beer$new() beer$percent_abv() ``` ``` ## [1] "5.0%" ``` ] --- # Socratic Break > I've already called created my class generator. Can I modify it? -- ```r Beer$rating <- 5 ``` > Well that was easy! -- ```r Beer$new()$rating ``` ``` ## NULL ``` > D'oh! I should use the generator's `set` method instead. -- ```r Beer$set("public", "rating", 5) Beer$new()$rating ``` ``` ## [1] 5 ``` > Woohoo! --- # Inheritance Methods and variables defined in one class (the parent) can be accessed in a subclass (the child). Pass an `R6ClassGenerator` name for the `inherit` parameter of your class call. ```r IPA <- R6::R6Class("IPA", inherit = Beer) IPA$new() ``` ``` ## <IPA> ## Inherits from: <Beer> ## Public: ## abv: 0.05 ## clone: function (deep = FALSE) ## percent_abv: function () ## rating: 5 ``` **Warning** The name is evaluated in the `parent_env` during instantiation. We will see this later, --- # Introspection Instantiated `R6` classes are assigned an `S3` class that reflects its `R6` hierarchy. ```r DoubleIPA <- R6::R6Class("DoubleIPA", inherit = IPA) dipa <- DoubleIPA$new() class(dipa) ``` ``` ## [1] "DoubleIPA" "IPA" "Beer" "R6" ``` -- .pull-left[ Unless you don't want it to do that. ```r NonAlcoholic <- R6::R6Class( "NonAlcoholic", inherit = Beer, class = FALSE) zima <- NonAlcoholic$new() class(zima) ``` ``` ## [1] "environment" ``` ] .pull-right[ .center[![](whywouldyoudothat.gif)] ] --- class: show-only-last-code-result # `private` Variables and methods that are part of the internal behavior of the class Useful to separate the `public` interface, similar to non-exported objects in a package. ```r Beer$set("private", ".name", "Duff") beer_new <- Beer$new() beer_new$.name ``` ``` ## NULL ``` -- .pull-left[ #### Internal Access using `private$` ```r Beer$set("public", "get_name", function() { private$.name }) beer_new <- Beer$new() beer_new$get_name() ``` ``` ## [1] "Duff" ``` ] .pull-right[ #### External If you just *have* to peek ```r beer_new$.__enclos_env__$private$.name ``` ``` ## [1] "Duff" ``` ] --- # `active` `R6` makes use of
's `makeActiveBinding` function to add active/lazy/delayed bindings. - No variable passed
Treated like a variable -- - One variable passed
Treated like an assignment -- - Two variables passed
Hey now, let's not get crazy. -- .pull-left[ #### No Variable ```r Beer$set("active", "name", function() { private$.name }) new_beer <- Beer$new() new_beer$name ``` ``` ## [1] "Duff" ``` ```r new_beer$name <- "Duff Lite" ``` ``` ## Error in (function () : unused argument (base::quote("Duff Lite")) ``` ] .pull-right[ #### One Variable ```r Beer$set("active", "name", function(n) { if (missing(n)) { return(private$.name) } stopifnot(is.character(n) && length(n) == 1) private$.name <- n }, * overwrite = TRUE ) new_beer <- Beer$new() new_beer$name <- "Duff Life" new_beer$name ``` ``` ## [1] "Duff Life" ``` ] --- # `initialize` The `initialize` method is called at the end of the generator's `new` function. ```r Beer$set("public", "initialize", function(name, rating = 5) { self$name <- name self$rating <- rating cli::cat_line(glue("It's five o'clock somewhere. Give me a {self$name}!")) }) beer <- Beer$new("Breakfast Stout") ``` ``` ## It's five o'clock somewhere. Give me a Breakfast Stout! ``` -- You can call the inherited `initialize` function using `super$`. ```r IPA$set("public", "initialize", function(name, ...) { name <- snakecase::to_any_case(name, 'random') super$initialize(name, ...) }) beer <- IPA$new("King Sue") ``` ``` ## It's five o'clock somewhere. Give me a KInG sUe! ``` --- # `initialize` If any public or private fields have reference semantics (other `R6` classes/`environment`s/`data.table`s) then they should be created in the `initialize` method to avoid sharing. Sometimes, this can be useful. .pull-left[ ```r `%||%` <- rlang::`%||%` SelfCounter <- R6::R6Class( "SelfCounter", public = list( count_env = new.env(), initialize = function() self$count_env$counter <- (self$count_env$counter %||% 0) + 1 ) ) ``` ] .pull-right[ ```r SelfCounter$new()$count_env$counter ``` ``` ## [1] 1 ``` ```r SelfCounter$new()$count_env$counter ``` ``` ## [1] 2 ``` ```r SelfCounter$new()$count_env$counter ``` ``` ## [1] 3 ``` ] --- # `initialize` .pull-left[ ```r LessUseful <- R6::R6Class( "LessUseful", public = list( count_env = NULL, initialize = function() { self$count_env <- new.env() self$count_env$counter <- (self$count_env$counter %||% 0) + 1 }) ) ``` ] .pull-right[ ```r LessUseful$new()$count_env$counter ``` ``` ## [1] 1 ``` ```r LessUseful$new()$count_env$counter ``` ``` ## [1] 1 ``` ```r LessUseful$new()$count_env$counter ``` ``` ## [1] 1 ``` ] --- # `finalize` The `finalize` method allows a class to clean up after itself. e.g., - Close database connections - Close file connections - Provide status updates ```r Beer$set("public", "finalize", function() { cat("Goodnight brew")}) beer <- IPA$new("Sculpin") ``` ``` ## It's five o'clock somewhere. Give me a sCulPIN! ``` ```r # we hardly knew ye rm(beer) # force a garbage collection to get the finalizer to trigger invisible(gc()) ``` ``` ## Goodnight brew ``` --- # `print` .pull-left[ ```r R6:::print.R6 ``` ``` ## function (x, ...) ## { ## if (is.function(.subset2(x, "print"))) { ## .subset2(x, "print")(...) ## } ## else { ## cat(format(x, ...), sep = "\n") ## } ## invisible(x) ## } ## <bytecode: 0x000000001d6655f8> ## <environment: namespace:R6> ``` ] .pull-right[ Define your own `print` in the `public` members of your class. ```r Beer$set( "public", "print", function(...) { cat( glue("{self$name}:{strrep('*',self$rating)}") ) }) ``` ```r beer <- Beer$new("Duff", 3) ``` ``` ## It's five o'clock somewhere. Give me a Duff! ``` ```r beer ``` ``` ## Duff:*** ``` ] A `format` generic is also defined for `R6` objects. --- # Cloning .pull-left[ ```r beer1 <- Beer$new("Duff") ``` ``` ## It's five o'clock somewhere. Give me a Duff! ``` ```r beer2 <- beer1 beer2$name <- "Duff Lite" beer2 ``` ``` ## Duff Lite:***** ``` ```r beer1 ``` ``` ## Duff Lite:***** ``` ] .pull-right[ ```r beer1 <- Beer$new("Duff") ``` ``` ## It's five o'clock somewhere. Give me a Duff! ``` ```r beer2 <- beer1$clone() beer2$name <- "Duff Lite" beer2 ``` ``` ## Duff Lite:***** ``` ```r beer1 ``` ``` ## Duff:***** ``` ] .footnote[Additional option `deep = TRUE` used when you want to make copies of all encapsulated objects that use reference semantics.] --- # Bottles ```r Beer$set("private", ".bottles", 9L) Beer$set("active", "bottles", function(b) { if (missing(b)) { return(private$.bottles) } private$.bottles <- b }) Beer$set("public", "drink", function() { if (self$bottles == 0) msg <- glue("No more bottles of {self$name} on the wall...\n") else { msg <- glue("{self$bottles} bottle{ifelse(self$bottles>1,'s','')} of {self$name}") msg <- glue("{msg} on the wall. {msg}! Take one down, pass it around.\n") self$bottles <- self$bottles - 1 } cli::cat_line(msg) invisible(self) }) beer <- Beer$new("Duff") ``` ``` ## It's five o'clock somewhere. Give me a Duff! ``` --- class: show-only-last-code-result bottles ```r beer$drink() ``` ``` ## 9 bottles of Duff on the wall. 9 bottles of Duff! Take one down, pass it around. ``` -- ```r beer$drink() ``` ``` ## 8 bottles of Duff on the wall. 8 bottles of Duff! Take one down, pass it around. ``` -- ```r beer$drink() ``` ``` ## 7 bottles of Duff on the wall. 7 bottles of Duff! Take one down, pass it around. ``` -- ```r beer$drink() ``` ``` ## 6 bottles of Duff on the wall. 6 bottles of Duff! Take one down, pass it around. ``` -- ```r beer$drink() ``` ``` ## 5 bottles of Duff on the wall. 5 bottles of Duff! Take one down, pass it around. ``` -- ```r beer$drink() ``` ``` ## 4 bottles of Duff on the wall. 4 bottles of Duff! Take one down, pass it around. ``` -- ```r beer$drink() ``` ``` ## 3 bottles of Duff on the wall. 3 bottles of Duff! Take one down, pass it around. ``` -- ```r beer$drink() ``` ``` ## 2 bottles of Duff on the wall. 2 bottles of Duff! Take one down, pass it around. ``` -- ```r beer$drink() ``` ``` ## 1 bottle of Duff on the wall. 1 bottle of Duff! Take one down, pass it around. ``` -- ```r beer$drink() ``` ``` ## No more bottles of Duff on the wall... ``` --- class: duffman # Method chaining If a method returns `self` then you can chain calls together. .left-column[ ```r Beer$ new("Duff")$ drink()$ drink()$ drink()$ drink()$ drink()$ drink()$ drink()$ drink()$ drink()$ drink() ``` ] .right-column[ ``` ## It's five o'clock somewhere. Give me a Duff! ## 9 bottles of Duff on the wall. 9 bottles of Duff! Take one down, pass it around. ## 8 bottles of Duff on the wall. 8 bottles of Duff! Take one down, pass it around. ## 7 bottles of Duff on the wall. 7 bottles of Duff! Take one down, pass it around. ## 6 bottles of Duff on the wall. 6 bottles of Duff! Take one down, pass it around. ## 5 bottles of Duff on the wall. 5 bottles of Duff! Take one down, pass it around. ## 4 bottles of Duff on the wall. 4 bottles of Duff! Take one down, pass it around. ## 3 bottles of Duff on the wall. 3 bottles of Duff! Take one down, pass it around. ## 2 bottles of Duff on the wall. 2 bottles of Duff! Take one down, pass it around. ## 1 bottle of Duff on the wall. 1 bottle of Duff! Take one down, pass it around. ## No more bottles of Duff on the wall... ``` ] --- # Capsule I had all intentions of demonstrating how R6 works under-the-hood using fancy diagrams like Hadley creates. Instead, I will monkey around in R. But, if you are interested and want to hurt your bra: [https://rpubs.com/sumprain/r6](https://rpubs.com/sumprain/r6)