Advanced chunks
NEST coreDev
2022-05-09
advanced_chunks.Rmd
Decoupling chunks containers
In a shiny app there is often a need to have multiple chunks
containers that may be used independently by certain parts of the
application and may be used together by other parts of the application.
The ability to have multiple chunks containers combined into one allows
for greater flexibility of the code, which allows the developer to apply
the DRY
principle (stands for Do not repeat
which means code that are used in multiple places should only be written
and defined once) and optimize performance. Much of the functionality to
use a single chunks container object independently has already been
covered in the Basic chunks vignette,
which you should read first before going any further.
Creating a chunks container and assigning it to a variable
In order to have multiple chunks containers, each must be accessible and distinguishable via a variable.
ch_0 <- teal.code::chunks_new()
ch_1 <- teal.code::chunks_new()
ch_2 <- teal.code::chunks_new()
# if data used is static, then it should be computed once
# isolating it into its own chunks container accomplishes this goal
ch_0$push(quote(df <- data.frame(a = runif(5, -10, 10), b = runif(5, -10, 10))))
ch_1$push(quote({
Sys.sleep(1) # some really long running process
foo_plot <- function(df_arg) plot(df_arg)
}))
ch_2$push(quote({
Sys.sleep(1) # some really long running process
foo_print <- function(df_arg) df_arg
}))
# chunks containers should be evaluated before being merged
# the reason for this will be explain later
teal.code::chunks_safe_eval(chunks = ch_0)
## a b
## 1 1.78274547 -1.5840153
## 2 9.61923385 -7.1294898
## 3 1.41337177 0.5395041
## 4 -0.05242216 5.5314265
## 5 0.10755724 -4.2628728
teal.code::chunks_safe_eval(chunks = ch_1)
## function(df_arg) plot(df_arg)
## <environment: 0x55c58bd3cb28>
teal.code::chunks_safe_eval(chunks = ch_2)
## function(df_arg) df_arg
## <environment: 0x55c58be50210>
The teal.code::chunks_push_chunks
function is called to
combine chunks containers together. Note that the base chunks container
to start combining from in the following examples are all empty.
However, that does not have to be the case. Starting with a clean slate
is easier to comprehend.
Perhaps a part of your shiny application needs to output a plot
ch_plot <- teal.code::chunks_new()
teal.code::chunks_push_chunks(chunks = ch_plot, x = ch_0)
teal.code::chunks_push_chunks(chunks = ch_plot, x = ch_1)
teal.code::chunks_push(chunks = ch_plot, expression = quote(foo_plot(df)))
teal.code::chunks_safe_eval(chunks = ch_plot)
## NULL
Perhaps a part of your shiny application needs to output a table
ch_table <- teal.code::chunks_new()
teal.code::chunks_push_chunks(chunks = ch_table, x = ch_0)
teal.code::chunks_push_chunks(chunks = ch_table, x = ch_2)
teal.code::chunks_push(chunks = ch_table, expression = quote(foo_print(df)))
teal.code::chunks_safe_eval(chunks = ch_table)
## a b
## 1 1.78274547 -1.5840153
## 2 9.61923385 -7.1294898
## 3 1.41337177 0.5395041
## 4 -0.05242216 5.5314265
## 5 0.10755724 -4.2628728
Perhaps the main part of your shiny application needs to output a plot, a table, and the code to generate both
ch_ouput <- teal.code::chunks_new()
teal.code::chunks_push_chunks(chunks = ch_ouput, x = ch_0)
teal.code::chunks_push_chunks(chunks = ch_ouput, x = ch_1)
teal.code::chunks_push_chunks(chunks = ch_ouput, x = ch_2)
teal.code::chunks_push(chunks = ch_ouput, expression = quote(foo_plot(df)))
teal.code::chunks_push(chunks = ch_ouput, expression = quote(foo_print(df)))
teal.code::chunks_safe_eval(chunks = ch_ouput)
## a b
## 1 1.78274547 -1.5840153
## 2 9.61923385 -7.1294898
## 3 1.41337177 0.5395041
## 4 -0.05242216 5.5314265
## 5 0.10755724 -4.2628728
code <- teal.code::chunks_get_rcode(chunks = ch_ouput)
cat(
paste(code, collapse = "\n")
)
## df <- data.frame(a = runif(5, -10, 10), b = runif(5, -10, 10))
## Sys.sleep(1)
## foo_plot <- function(df_arg) plot(df_arg)
## Sys.sleep(1)
## foo_print <- function(df_arg) df_arg
## foo_plot(df)
## foo_print(df)
When chunks containers are merged together, their code expressions are combined in the order that the containers were merged and their environments are combined into a single environment.
Even in the event that the expressions that are combined will lead to errors being thrown when evaluated, there is no way for the chunks object to know that before evaluation. Hence code expressions will not interrupt the merging of chunks containers together.
However, the potential conflict of when two chunks containers have the same variables which holds different values in their respective environments will throw an error when merging chunks containers.
ch_0 <- teal.code::chunks_new()
ch_1 <- teal.code::chunks_new()
ch_0$push(quote(a_var <- 0))
ch_1$push(quote(a_var <- 0))
teal.code::chunks_safe_eval(chunks = ch_0)
## [1] 0
teal.code::chunks_safe_eval(chunks = ch_1)
## [1] 0
# OK - operation is successful as the variable a_var holds the same value in both chunks
teal.code::chunks_push_chunks(chunks = ch_0, x = ch_1)
ch_2 <- teal.code::chunks_new()
ch_2$push(quote(a_var <- 1))
teal.code::chunks_safe_eval(chunks = ch_2)
## [1] 1
# ERROR - operation is rejected as the variable a_var holds different values
tryCatch(
teal.code::chunks_push_chunks(chunks = ch_0, x = ch_2),
error = function(e) e
)
## <simpleError in chunks$push_chunks(x = x, overwrite = overwrite): chunks_push_chunks does not allow overwriting already calculated values.
## Following variables would have been overwritten:
## - a_var>
Why should you evaluate before merging?
With the exception of code that is meant to return something at the end of a function, code usually modifies the environment and / or the variables inside the environment.
When chunks containers have code that have not been evaluated, these code will not block any merging operations as explained above. But if they are evaluated, then their environment will be updated and will reflect the intended state of the chunks container as described by the code expressions.
And as described above, it is the environment that will block the operation to merge chunks containers together when there are conflicts. So evaluating before merging will prevent some bugs in your app.
ch_0 <- teal.code::chunks_new()
ch_0$push(quote(a_var <- 0))
teal.code::chunks_safe_eval(chunks = ch_0)
## [1] 0
ch_2 <- teal.code::chunks_new()
ch_2$push(quote(a_var <- 1))
# note that ch_2 is not evaluated
# OK - operation is successful
teal.code::chunks_push_chunks(chunks = ch_0, x = ch_2)
Cloning a chunks container
A chunks container is an R6 reference class, so by default contains a
clone
method. A deep copy is needed if some part of your
app needs to start from the state of an already existing chunks
container and modify or add on to it, but some other part of your app
still needs the original chunks container unaltered.
The example code below is another way to print the plot above, which starts combining chunks from a non-empty chunks container derived from cloning another chunks container
ch_0 <- teal.code::chunks_new()
ch_0$push(quote({
df <- data.frame(a = runif(5, -10, 10), b = runif(5, -10, 10))
foo_plot <- function(df_arg) plot(df_arg)
}))
teal.code::chunks_safe_eval(chunks = ch_0)
## function(df_arg) plot(df_arg)
## <environment: 0x55c58cf85370>
ch_plot_clone <- teal.code::chunks_deep_clone(ch_0)
teal.code::chunks_push(chunks = ch_plot_clone, expression = quote(foo_plot(df)))
teal.code::chunks_safe_eval(chunks = ch_plot_clone)
## NULL
Extracting values of variables in a chunks container
In order to extract values of variables inside a chunks container, the code that generates that variable must have been evaluated
ch_0 <- teal.code::chunks_new()
ch_0$push(quote(df <- data.frame(a = runif(5, -10, 10), b = runif(5, -10, 10))))
teal.code::chunks_safe_eval(chunks = ch_0)
## a b
## 1 7.440171 1.849777
## 2 8.350948 8.888268
## 3 -4.277250 3.613847
## 4 -6.229418 -7.860479
## 5 2.300668 -2.137086
env <- new.env()
env$df <- teal.code::chunks_get_var(var = "df", chunks = ch_0)
# Once extracted, the value can be passed into other chunks containers if needed
ch_print_df <- teal.code::chunks_new()
teal.code::chunks_reset(envir = env, chunks = ch_print_df)
teal.code::chunks_push(chunks = ch_print_df, expression = quote(df))
teal.code::chunks_safe_eval(chunks = ch_print_df)
## a b
## 1 7.440171 1.849777
## 2 8.350948 8.888268
## 3 -4.277250 3.613847
## 4 -6.229418 -7.860479
## 5 2.300668 -2.137086
Example
The following is a shiny app that uses the chunks container object in
ways that mirror how a sophistical teal
app would use it.
This app constructs several chunks containers, allowing each to be used
and combined by different parts of the app, which enables the app to
apply the DRY
principle and optimize performance.
library(shiny)
library(teal.code)
library(dplyr)
library(rlang)
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
# Input of Response can be chosen from Event Table
selectInput(
inputId = "filter_paramcd",
label = "Filter Paramcd",
choices = c("OS", "EFS", "PFS"),
selected = "OS",
multiple = TRUE
),
selectInput(
inputId = "filter_sex",
label = "Filter sex",
choices = c("F", "M", "U"),
selected = c("F", "M", "U"),
multiple = TRUE
),
selectInput(
inputId = "anl_columns",
label = "ANL columns",
choices = NULL,
multiple = TRUE
)
),
mainPanel(
verbatimTextOutput("code"),
plotOutput("plot"),
DT::DTOutput("anl_table")
)
)
)
server <- function(input, output, session) {
# chunks container to hold the datasets used in this app.
# The purpose of storing the code to generate the datasets is reproducibility.
data_def_chunks <- chunks_new()
chunks_push(
chunks = data_def_chunks,
expression = quote({
library(shiny)
library(teal.code)
library(dplyr)
library(rlang)
})
)
chunks_push(
chunks = data_def_chunks,
expression = quote(
adsl <- list(
SUBJID = 1:100,
STUDYID = c(rep(1, 20), rep(2, 50), rep(3, 30)),
AGE = sample(20:88, 100, replace = T) %>% as.numeric(),
SEX = sample(c("M", "F", "U"), 100, replace = T) %>% as.factor()
) %>%
as_tibble()
)
)
chunks_push(
chunks = data_def_chunks,
expression = quote(
events <- list(
SUBJID = rep(1:100, 3),
STUDYID = rep(c(rep(1, 20), rep(2, 50), rep(3, 30)), 3),
PARAMCD = c(rep("OS", 100), rep("EFS", 100), rep("PFS", 100)),
AVAL = c(rexp(100, 1 / 100), rexp(100, 1 / 80), rexp(100, 1 / 60)) %>% as.numeric(),
AVALU = rep("DAYS", 300) %>% as.factor()
) %>%
as_tibble()
)
)
# evaluating the code so that the datasets become available
chunks_safe_eval(data_def_chunks)
# reactive that needs to extract a value from another chunks container
adsl_filtered_reactive <- reactive({
env <- new.env()
env$adsl <- chunks_get_var(var = "adsl", chunks = data_def_chunks)
ch <- chunks_new()
chunks_reset(chunks = ch, env = env)
chunks_push(
chunks = ch,
expression = expr(adsl_filtered <- adsl %>% dplyr::filter(SEX %in% !!input$filter_sex))
)
chunks_safe_eval(ch)
ch
})
# reactive that needs to extract a value from another chunks container
events_filtered_reactive <- reactive({
env <- new.env()
env$events <- chunks_get_var(var = "events", chunks = data_def_chunks)
ch <- chunks_new()
chunks_reset(chunks = ch, env = env)
chunks_push(
chunks = ch,
expression = expr(events_filtered <- events %>% dplyr::filter(PARAMCD %in% !!input$filter_paramcd))
)
chunks_safe_eval(ch)
ch
})
# part of the app that merges two other chunks containers
merge_data <- reactive({
ch <- chunks_new()
chunks_push_chunks(chunks = ch, x = adsl_filtered_reactive())
chunks_push_chunks(chunks = ch, x = events_filtered_reactive())
chunks_push(
chunks = ch,
expression = expr(anl <- left_join(adsl_filtered, events_filtered, by = c("STUDYID", "SUBJID")))
)
chunks_safe_eval(ch)
ch
})
observeEvent(merge_data(), {
anl <- chunks_get_var("anl", merge_data())
updateSelectInput(
session,
"anl_columns",
choices = colnames(anl),
selected = colnames(anl)
)
})
# need to clone as chunks are R6 object - this would change ch in merge_data()
subset_anl <- reactive({
ch <- chunks_deep_clone(merge_data())
chunks_push(
chunks = ch,
expression = rlang::expr(anl_subset <- anl[!!input$anl_columns])
)
chunks_safe_eval(ch)
ch
})
# a chunks container that needs to extract a value from another chunks container
table_call <- reactive({
env <- new.env()
env$anl_subset <- chunks_get_var(chunks = subset_anl(), var = "anl_subset")
ch <- chunks_new()
chunks_reset(chunks = ch, env = env)
chunks_push(
chunks = ch,
expression = rlang::expr({
table <- DT::datatable(anl_subset)
table
})
)
chunks_safe_eval(ch)
ch
})
# a chunks container that needs to extract a value from another chunks container
plot_call <- reactive({
env <- new.env()
env$anl_subset <- chunks_get_var(chunks = subset_anl(), var = "anl_subset")
ch <- chunks_new()
chunks_reset(chunks = ch, env = env)
chunks_push(
chunks = ch,
expression = rlang::expr({
plot_output <- plot(anl_subset$AGE, anl_subset$AVAL)
plot_output
})
)
chunks_safe_eval(ch)
ch
})
# Really long running process that should not be recomputed if can be avoided.
#
# The ability to put this process in its own chunks container, independent of the rest,
# allows the app to avoid re-computation and yet be able to capture its code to ensure reproducibility.
sleep_5_seconds <- reactive({
ch <- chunks_new()
chunks_push(
chunks = ch,
expression = quote(Sys.sleep(5))
)
chunks_safe_eval(ch)
ch
})
# part of the app that needs to combine all chunks containers
outputs_call <- reactive({
ch <- chunks_new()
chunks_push_chunks(chunks = ch, x = data_def_chunks)
chunks_push_chunks(chunks = ch, x = sleep_5_seconds())
chunks_push_chunks(chunks = ch, x = subset_anl())
chunks_push_chunks(chunks = ch, x = plot_call())
chunks_push_chunks(chunks = ch, x = table_call())
ch
})
# part of the app that only needs a single chunks container
output$anl_table <- DT::renderDT(chunks_get_var("table", table_call()))
output$plot <- renderPlot(chunks_get_var("plot_output", plot_call()))
output$code <- renderPrint({
cat(
paste(
chunks_get_rcode(chunks = outputs_call()),
collapse = "\n"
)
)
})
}
shinyApp(ui, server)