{tidymodules}
vignettes/intro.Rmd
intro.Rmd
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:
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
#> <Calculator> object generator
#> Public:
#> value: NA
#> initialize: function (value)
#> add: function (x = 1)
#> clone: function (deep = FALSE)
#> Parent env: <environment: R_GlobalEnv>
#> Locked objects: TRUE
#> Locked class: FALSE
#> Portable: TRUE
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
#> <Calculator>
#> Public:
#> add: function (x = 1)
#> clone: function (deep = FALSE)
#> initialize: function (value)
#> value: 0
You can then call the methods and access fields using
$
:
calculator1$add(10)
calculator1$value
#> [1] 10
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
#> <Calculator> object generator
#> Public:
#> value: NA
#> initialize: function (value)
#> add: function (x = 1)
#> clone: function (deep = FALSE)
#> subtract: function (x = 1)
#> Parent env: <environment: R_GlobalEnv>
#> Locked objects: TRUE
#> Locked class: FALSE
#> Portable: TRUE
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
class
The 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}
module
You 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.