knitr::opts_chunk$set( collapse = TRUE, eval = TRUE, comment = "#>" )
{tidymodules}
or {tm}
in short, is based on R6 (https://r6.r-lib.org/), which is an implementation of encapsulated object-oriented programming for R. Therefore knowledge of R6 is a prerequisite to develop {tm}
modules.
R6 provides a framework for OOP in R. Unlike the functional programming style, R6 encapsulates methods and fields in classes that instantiate into objects. R6 classes are similar to R's reference classes, but are more efficient and do not depend on S4 classes and the methods package.
This vignette provides a brief overview of R6 for developers new to R6. For more information, developers are recommended to review the R6 packagedown site (https://r6.r-lib.org/), as well as Chapter 14 of the Advanced R book (https://adv-r.hadley.nz/r6.html)
{tm}
depends on the R6 package which you can install from CRAN and load:
library(R6)
R6 classes are created using the R6::R6Class()
function, which is the only function from the R6 package that is typically used. The following is a simple example of defining an R6 class:
Calculator <- R6::R6Class( classname = "Calculator", public = list( value = NA, initialize = function(value) { self$value <- value }, add = function(x = 1) { self$value <- self$value + x invisible(self) } ) )
The first argument classname
by convention uses UpperCamelCase
. The second argument public
encapsulates a list of methods (functions) and fields (objects) that make up the public interface of the object. By convention methods and fields use snake_case
. Methods can access the methods and fields of the current object using self$
. One should always assign the result of R6Class()
into a variable with the same names as the classname
because R6Class()
returns an R6 object that defines the class.
You can print the class definition:
Calculator
To create a new instance of Calculator
, use the $new()
method. The $initialize()
is an important method, which overrides the default behavior of $new()
. In the above example, the $initialize()
method initializes the calculator1
object with value = 0
.
calculator1 <- Calculator$new(0) calculator1
You can then call the methods and access fields using $
:
calculator1$add(10) calculator1$value
You can also add methods after class creation as illustrated below for the existing Calculator
R6 class, although new methods and fields are only available to new objects.
Calculator$set("public", "subtract", function(x = 1) { self$value <- self$value - x invisible(self) }) Calculator
Below are some key features of R6.
$clone()
method for making copy of an object. For more details, refer to https://r6.r-lib.org/reference/R6Class.html#cloning-objects. R6Class()
has a private
argument for you to define private methods and fileds that can only be accessed from within the class, not from the outside.super$
.tidymodules::TidyModule
classThe tidymodules::TidyModule
class is a R6 class and the parent of all {tm}
modules.
Below is partial code of the TidyModule
class for illustration purpose. The TidyModule
class includes many public methods. There are utility functions such as callModules()
, definePorts()
, assignPort()
as well as functions that need to be overwritten such as ui()
, server()
, etc.
Unlike conventional Shiny modules in funtional programming style, {tm}
encapsulates functions such as ui() and server() as methods in a TidyModule class object. Module namespace ID is seamlessly managed within the module class for the ui and server. For complete technical documentation that includes other methods and fields, see ?TidyModule
.
TidyModule <- R6::R6Class( "TidyModule", public = list( id = NULL, module_ns = NULL, parent_ns = NULL, parent_mod = NULL, parent_ports = NULL, group = NULL, created = NULL, o = NULL, i = NULL, initialize = function(id = NULL, inherit = TRUE, group = NULL) { # details omitted }, # Other methods such ui = function() { return(shiny::tagList()) }, server = function(input, output, session) { # Need to isolate this block to avoid unecessary triggers shiny::isolate({ private$shiny_session <- session private$shiny_input <- input private$shiny_output <- output }) }, definePort = function(x) { shiny::isolate(x) }, assignPort = function(x) { shiny::observe({ shiny::isolate(x) }) }, # Other public methods omitted ), private = list( # Details omitted ) )
{tm}
moduleYou can develop new {tm}
modules by inheriting and extending the tidymodules::TidyModule
class.
Below is a minimal example, RandomNumberGenerator
, defined with one input port and one output port. The input port is a random number seed that feeds into a random number generator, whose result serves as the module output.
# Module definition RandomNumMod <- R6::R6Class( "RandomNumGenerator", inherit = tidymodules::TidyModule, public = list( initialize = function(id = NULL) { super$initialize(id) self$definePort({ self$addInputPort( name = "seed", description = "random number seed", sample = 123 ) self$addOutputPort( name = "number", description = "Random number", sample = 123 ) }) }, ui = function() { tagList( verbatimTextOutput(self$ns("text")) ) }, server = function(input, output, session) { super$server(input, output, session) result <- reactive({ s <- self$getInput("seed") set.seed(s()) floor(runif(1) * 1e5) }) output$text <- renderPrint({ s <- self$getInput("seed") print(paste0("seed = ", s())) print(paste0("number = ", result())) }) self$assignPort({ self$updateOutputPort( id = "number", output = result ) }) return(result) } ) )
Cross-communication between two {tm}
modules is established using several flavours of the pipe %>%
operator, as illustrated in the following code. The first module's output is fed as the random number seed for the second module.
## Calling app randomNumMod1 <- RandomNumMod$new() randomNumMod2 <- RandomNumMod$new() ui <- tagList( fluidPage( randomNumMod1$ui(), randomNumMod2$ui() ) ) server <- function(input, output, session) { randomNumMod1$callModule() randomNumMod2$callModule() seed_1 <- reactive(123) observe({ seed_1 %>1% randomNumMod1 %1>1% randomNumMod2 }) } shinyApp(ui = ui, server = server)
To learn more about writing {tm}
modules, read the examples.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.