Reactive object bindings with built-in caching and push functionality
require("devtools")
devtools::install_github("Rappster/conditionr")
devtools::install_github("Rappster/yamlr")
devtools::install_github("Rappster/typr")
devtools::install_github("Rappster/reactr")
require("reactr")
The package aims at contributing to Reactive Programming or Reactivity in R.
It allows the specification of reactive objects based on reactive expressions that dynamically bind them to other objects.
That way, an object x
can be dynamically observed by n
other objects. Whenever x
changes, the n
variables observing x
change according to their reactive expressions that defines the actual binding relationship. This is an approach to ensure consistent states of objects and the entire system. You can choose how changes should be propagated throughout the system: following a pull or a push principle (see section Highlighting selected features).
Two different reactivity implementations are provided:
shiny
(recommended for regular reactive scenarios)I tried to build as much on top of existing functionality in order to make this package as compatible as possible with shiny apps. However, I was not able to implement all of my "wish-list features" yet that way - so any help is greatly appreciated.
Branch legacy-shinyOld
contains a legacy version on the road to the current implementation that might or might not be of interest to other developers.
This implementation is older and is much more "pedestrian" as it stems from the time where I did not understand very well yet how reactivity is implemented in shiny
. However, while it is quite "custom-made" in comparision, it does work and on top reveals some of the interesting details that shiny solves in a similar, yet much more elegantly way. Thus I decided to keep it as a reference for myself and other programmers for the time being.
The package is greatly inspired by reactivity as implemented by the shiny framework.
setShinyReactive(id = "x_1", value = 10)
setShinyReactive(id = "x_2", value = reactiveExpression(x_1 * 2))
x_1
x_2
## --> x_1 * 2 = 20
(x_1 <- 20)
x_2
## --> x_1 * 2= 40
setShinyReactive(id = "root_dir", value = getwd())
setShinyReactive(id = "sub_dir", value = reactiveExpression(
file.path(root_dir, "doc")
))
root_dir
sub_dir
## --> "Q:/home/wsp/rapp2/reactr/doc"
root_dir <- tempdir()
sub_dir
## --> "C:\\Users\\jat\\AppData\\Local\\Temp\\RtmpyUOCMk/doc"
Clean up
rmReactive("x_1")
rmReactive("x_2")
Strictness levels can be defined for
the creation process itself in setReactive()
andsetShinyReactive()
: see argument strict
strict_get
strict_set
See vignette Strictness for details.
When using a pull paradigm (the default), objects referencing other objects that have changed are not informed of these change until they are explicitly requested (by get()
or its syntactical sugars).
When using a push paradigm, an object that changed informs all objects that have a reference to it about the change by implicitly calling the $getVisible()
method of all of their registered push references.
See vignette Pushing for details on this.
Summary of the added functionality compared to what is currently offered by existing shiny functionality (shiny's limitations should always be read "AFAIK" ;-)):
The same function can be used for setting both reative sources and observers.
Reactive expressions/binding functions are hidden from the user.
To the user, all reactive objects appear and behave as if they are actual non-reactive/non-functional values. This eliminates the need to distinguish (mentally and by code) if a certain value is a non-functional value or a function that needs to be executed via ()
.
The latter is what is necessary when using current shiny functionality based on shiny::makeReactiveBinding()
and shiny::reactive()
).
However, you can mimick the default shiny behavior by setting lazy = TRUE
.
Push updates
While shiny implements reactivity following a pull paradigm with respect to the way that changes are propagated throughout the system (resembles lazy evaluation), reactr
also offers the alternative use of a push paradigm where changes are actively propagated via the argument push = TRUE
.
See vignette Relations to Shiny for more details.
setReactive()
):The preferred way to specify the reference is via YAML markup as in the example above. However, there also exist two other ways to specify references.:
refs
.Via explicit get()
calls in the body of form
.ref_{number} <- get({id}, {where})
with {number}
being an arbitrary number or other symbol, {id}
being the referenced object's name/ID and {where}
being the environment where the value belonging to {id}
was assigned to (e.g. .ref_1 <- get{"x_1", where_1}
).
See vignette Reactive References for details.
setReactive()
): Binding functions are only executed if they need to be, i.e. only if one of the referenced objects has actually changed.
Otherwise a cached value that has been stored from the last update run is returned.
While this may cost more than it actually helps in scenarios where the binding functions are quite simple and thus don't take long to run, caching may reduce runtimes/computation times in case of either more complex and long-running binding functions or when greater amounts of data comes into play (needs to be tested yet).
See vignette Caching for details.
## Basics //
## Strict = 0:
setShinyReactive(id = "x_1", value = 10, typed = TRUE)
x_1 <- "hello world!"
x_1
## --> simply ignored
## Strict = 1:
setShinyReactive(id = "x_1", value = 10, typed = TRUE, strict = 1)
try(x_1 <- "hello world!")
x_1
## --> ignored with warning
## Strict = 2:
setShinyReactive(id = "x_1", value = 10, typed = TRUE, strict = 2)
try(x_1 <- "hello world!")
x_1
## --> ignored with error
## Advanced //
setShinyReactive(id = "x_1", typed = TRUE, from_null = FALSE, strict = 2)
try(x_1 <- "hello world!")
setShinyReactive(id = "x_1", value = 10, typed = TRUE, to_null = FALSE, strict = 2)
try(x_1 <- NULL)
setShinyReactive(id = "x_1", value = 10, typed = TRUE, numint = FALSE, strict = 2)
try(x_1 <- as.integer(10))
Such a construct could be used for logging or ensuring that certain database operations are triggered right away after the system state has changed:
setShinyReactive(id = "x_1", value = 10)
setShinyReactive(
id = "x_2",
value = reactiveExpression({
message(paste0("[", Sys.time(), "] I'm x_2 and the value of x_1 is: ", x_1))
x_1 * 2
}),
push = TRUE
)
## --> [2014-11-13 17:35:11] I'm x_2 and the value of x_1 is: 10
x_1
## --> 10
x_2
## --> 20
Note that we never request the value of x_2
explicitly yet changes in x_1
are actively pushed to x_2
thus executing its reactive binding function:
(x_1 <- 11)
## --> [2014-11-13 17:35:47] I'm x_2 and the value of x_1 is: 11
## --> 11
(x_1 <- 12)
## --> [2014-11-13 17:36:14] I'm x_2 and the value of x_1 is: 12
## --> 12
(x_1 <- 13)
## --> [2014-11-13 17:36:32] I'm x_2 and the value of x_1 is: 13
## --> 13
x_2
## --> 26
Clean up
rmReactive("x_1")
rmReactive("x_2")
Specify reactive objects:
setShinyReactive(id = "x_1", value = 1:5, typed = TRUE)
setShinyReactive(id = "x_2", value = reactiveExpression(x_1 * 2), typed = TRUE)
setShinyReactive(id = "x_3", value = reactiveExpression(
data.frame(x_1 = x_1, x_2 = x_2)), typed = TRUE)
setShinyReactive(id = "x_4", value = reactiveExpression(
list(
x_1 = summary(x_1),
x_2 = summary(x_2),
x_3_new = data.frame(x_3, prod = x_3$x_1 * x_3$x_2),
filenames = paste0("file_", x_1)
)
))
Inspect:
x_1
# [1] 1 2 3 4 5
x_2
# [1] 2 4 6 8 10
x_3
# x_1 x_2
# 1 1 2
# 2 2 4
# 3 3 6
# 4 4 8
# 5 5 10
x_4
# $x_1
# Min. 1st Qu. Median Mean 3rd Qu. Max.
# 1 2 3 3 4 5
#
# $x_2
# Min. 1st Qu. Median Mean 3rd Qu. Max.
# 2 4 6 6 8 10
#
# $x_3_new
# x_1 x_2 prod
# 1 1 2 2
# 2 2 4 8
# 3 3 6 18
# 4 4 8 32
# 5 5 10 50
#
# $filenames
# [1] "file_1" "file_2" "file_3" "file_4" "file_5"
Change values and inspect implications:
(x_1 <- 100:102)
# [1] 100 101 102
x_2
# [1] 200 202 204
x_3
# x_1 x_2
# 1 100 200
# 2 101 202
# 3 102 204
x_4
# $x_1
# Min. 1st Qu. Median Mean 3rd Qu. Max.
# 100.0 100.5 101.0 101.0 101.5 102.0
#
# $x_2
# Min. 1st Qu. Median Mean 3rd Qu. Max.
# 200 201 202 202 203 204
#
# $x_3_new
# x_1 x_2 prod
# 1 100 200 20000
# 2 101 202 20402
# 3 102 204 20808
#
# $filenames
# [1] "file_100" "file_101" "file_102"
(x_1 <- 1)
# [1] 1
x_2
# [1] 2
x_3
# x_1 x_2
# 1 1 2
x_4
# $x_1
# Min. 1st Qu. Median Mean 3rd Qu. Max.
# 1 1 1 1 1 1
#
# $x_2
# Min. 1st Qu. Median Mean 3rd Qu. Max.
# 2 2 2 2 2 2
#
# $x_3_new
# x_1 x_2 prod
# 1 1 2 2
#
# $filenames
# [1] "file_1"
try((x_1 <- "hello world!"))
x_1
## --> still `1:2` --> overwrite has been ignored (due to `typed = TRUE`)
Clean up:
rmReactive("x_1")
rmReactive("x_2")
rmReactive("x_3")
rmReactive("x_4")
Reference Classes:
TestRefClass <- setRefClass("TestRefClass",
fields = list(x_1 = "numeric", x_2 = "numeric"))
inst <- TestRefClass$new()
class(inst)
setShinyReactive(id = "x_1", value = 10, where = inst)
setShinyReactive(id = "x_2",
value = reactiveExpression(inst$x_1 * 2), where = inst)
inst$x_1
inst$x_2
(inst$x_1 <- 20)
inst$x_2
R6 Classes:
TestR6 <- R6Class("TestR6", public = list(x_1 = "numeric", x_2 = "numeric"))
setOldClass(c("TestR6", "R6"))
inst <- TestR6$new()
class(inst)
setShinyReactive(id = "x_1", value = 10, where = inst)
setShinyReactive(id = "x_2",
value = reactiveExpression(inst$x_1 * 2), where = inst)
inst$x_1
inst$x_2
(inst$x_1 <- 20)
inst$x_2
Note that we set verbose = TRUE
to enable the display of status messages that help understand what's going on.
Set reactive object x_1
that others can reference:
setReactive(id = "x_1", value = 10, verbose = TRUE)
Set reactive object that references x_1
and has a reactive binding of form x_1 * 2
to it:
setReactive(id = "x_2", value = function() {
"object-ref: {id: x_1}"
x_1 * 2
}, verbose = TRUE)
# Initializing ...
x_1
# [1] 10
x_2
# [1] 20
Whenever x_1
changes, x_2
changes accordingly:
(x_1 <- 100)
# [1] 100
x_2
# Object: ab22808532ff42c87198461640612405
# Called by: ab22808532ff42c87198461640612405
# Modified reference: 2fc2e352f72008b90a112f096cd2d029
# - Checksum last: 2522027d230e3dfe02d8b6eba1fd73e1
# - Checksum current: d344558826c683dbadec305ed64365f1
# Updating ...
# [1] 200
See the examples of setReactive()
for a short description of the information contained in the status messages
Note that for subsequent requests and as long as x_1
does not change, the value that has been cached during the last update cycle is used instead of re-running the binding function each time:
x_2
# [1] 200
## --> cached value, no update
x_2
# [1] 200
## --> cached value, no update
(x_1 <- 1)
x_2
# Object: ab22808532ff42c87198461640612405
# Called by: ab22808532ff42c87198461640612405
# Modified reference: 2fc2e352f72008b90a112f096cd2d029
# - Checksum last: d344558826c683dbadec305ed64365f1
# - Checksum current: 6717f2823d3202449301145073ab8719
# Updating ...
# [1] 2
## --> update according to binding function
x_2
# [1] 2
## --> cached value, no update
Clean up
rmReactive("x_1")
rmReactive("x_2")
This turns reactive objects (that are, even though hidden from the user, instances of class ReactiveObject.S3
) into regular or non-reactive objects again.
Note that it does not mean the a reactive object is removed alltogether! See rmReactive()
for that purpose
setReactive(id = "x_1", value = 10)
setReactive(id = "x_2", value = function() "object-ref: {id: x_1}")
## Illustrate reactiveness //
x_1
x_2
(x_1 <- 50)
x_2
## Unset reactive --> turn it into a regular object again //
unsetReactive(id = "x_1")
Illustration of removed reactiveness:
x_1
x_2
(x_1 <- 10)
x_2
## --> `x_1` is not a reactive object anymore; from now on, `x_2` simply returns
## the last value that has been cached
What happens when a reactive relationship is broken or removed depends on how you set argument strictness_get
in the call to setReactive()
or setShinyReactive()
.
Also refer to vignette Strictness for more details.
This deletes the object alltogether.
setReactive(id = "x_1", value = 10)
setReactive(id = "x_2", value = function() "object-ref: {id: x_1}")
## Remove reactive --> remove it from `where` //
rmReactive(id = "x_1")
exists("x_1", inherits = FALSE)
The examples currently still use setReactive()
instead of the recommended setShinyReactive()
but should also work for setShinyReactive()
given that you specify value
via reactiveExpression()
and that you do not want to use bi-directional bindings (as this is currently only possible when using setReactive()
)
A
references B
A
uses value of B
"as is", i.e. value of A
identical to value of B
Set object x_1
that others can reference:
setReactive(id = "x_1", value = 10)
Set object that references x_1
and has a reactive binding to it:
setReactive(id = "x_2", value = function() "object-ref: {id: x_1}")
x_1
x_2
Whenever x_1
changes, x_2
changes accordingly:
(x_1 <- 100)
# [1] 100
x_2
# [1] 100
x_2
# [1] 100
## --> cached value as `x_1` has not changed; no update until `x_1`
## changes again
## Clean up //
rmReactive("x_1")
rmReactive("x_2")
A
references B
A
transforms value of B
, i.e. value of A
is the result of applying a function on the value of B
setReactive(id = "x_1", value = 10)
setReactive(id = "x_2", value = function() "object-ref: {id: x_1}")
setReactive(id = "x_3", value = function() {
"object-ref: {id: x_1, as: ref_1}"
ref_1 * 2
})
Note how x_3
changes according to its binding relationship ref_1 * 2
(which is just a translation for x_1 * 2
):
x_1
# [1] 10
x_2
# [1] 10
x_3
# [1] 20
## --> x_1 * 2
(x_1 <- 500)
x_2
# [1] 500
x_3
# [1] 1000
## Clean up //
rmReactive("x_1")
rmReactive("x_2")
rmReactive("x_3")
A
references B
and C
, B
references C
A
transforms value of B
, i.e. value of A
is the result of applying a function on the value of B
setReactive(id = "x_1", value = 10)
setReactive(id = "x_2", value = function() "object-ref: {id: x_1}")
setReactive(id = "x_3", value = function() {
"object-ref: {id: x_1, as: ref_1}"
"object-ref: {id: x_2, as: ref_2}"
ref_1 + ref_2 * 2
})
Note how each object that is involved changes according to its binding relationships:
x_3
# [1] 30
(x_1 <- 100)
x_3
[1] 300
(x_2 <- 1)
x_2
## --> disregarded as `x_2` has a one-directional binding to `x_1`, hence does
## not accept explicit assignment values
x_3
# [1] 300
(x_1 <- 50)
x_2
# [1] 50
x_3
# [1] 150
## Clean up //
rmReactive("x_1")
rmReactive("x_2")
rmReactive("x_3")
A
references B
and B
references A
--> bidirectional binding type
A
uses value of B
"as is" and B
uses value of A
"as is". This results in a steady state.
A cool feature of this binding type is that you are free to alter the values of both objects and still keep everything "in sync"
setReactive(id = "x_1", function() "object-ref: {id: x_2}")
setReactive(id = "x_2", function() "object-ref: {id: x_1}")
Note that the call to setReactive()
merely initializes objects with bidirectional bindings to the value numeric(0)
:
x_1
# NULL
x_2
# NULL
You must actually assign a value to either one of them via <-
after establishing the binding:
## Set actual initial value to either one of the objects //
(x_1 <- 100)
# [1] 100
x_2
# [1] 100
x_1
# [1] 100
## Changing the other one of the two objects //
(x_2 <- 1000)
# [1] 1000
x_1
# [1] 1000
## Clean up //
rmReactive("x_1")
rmReactive("x_2")
A
references B
and B
references A
--> bidirectional binding type
A
uses transformed value of B
and B
uses transformed value of A
.
The binding functions used result in a steady state.
As the binding functions are "inversions"" of each other, we still get to a steady state.
setReactive(id = "x_1", function() {
"object-ref: {id: x_2}"
x_2 * 2
})
setReactive(id = "x_2", function() {
"object-ref: {id: x_1}"
x_1 / 2
})
Note that due to the structure of the binding functions, the visible object values are initialized to numeric()
instead of NULL
now.
x_1
# numeric(0)
x_2
# numeric(0)
Here, we always reach a steady state, i.e. a state in which cached values can be used instead of the need to executed the binding functions.
## Set actual initial value to either one of the objects //
(x_1 <- 100)
# [1] 100
x_2
# [1] 50
x_1
# [1] 100
## Changing the other one of the two objects //
(x_2 <- 1000)
# [1] 1000
x_1
# [1] 2000
x_2
# [1] 1000
## Clean up //
rmReactive("x_1")
rmReactive("x_2")
A
references B
and B
references A
--> bidirectional binding type
A
uses transformed value of B
and B
uses transformed value of A
.
The binding functions used result in a non-steady state.
As the binding functions are not "inversions"" of each other, we never reach/stay at a steady state. Cached values are/can never be used as by the definition of the binding functions the two objects are constantly updating each other.
setReactive(id = "x_1", function() {
"object-ref: {id: x_2}"
x_2 * 2
})
setReactive(id = "x_2", function() {
"object-ref: {id: x_1}"
x_1 * 10
})
Here, we have "non-steady-state" behavior, i.e. we never reach a state were cached values can be used. We always need to execute the binding functions as each request of a visible object value results in changes.
This is best verified when using verbose = TRUE
and comparing it to the other scenarios (not done at this point).
x_1
# numeric(0)
x_2
# numeric(0)
## Set actual initial value to either one of the objects //
(x_1 <- 1)
# [1] 1
x_2
# [1] 10
## --> `x_1` * 10
x_1
# [1] 20
## --> x_2 * 2
x_2
# [1] 200
## --> `x_1` * 10
## Changing the other one of the two objects //
(x_2 <- 1)
# [1] 1
x_1
# [1] 2
x_2
# [1] 20
x_1
# [1] 40
## Clean up //
rmReactive("x_1")
rmReactive("x_2")
This is only necessary/usefull when using setReactive()
as setShinyReactive()
as shiny takes care of registering and caching itself.
The package implements a caching mechanism that (hopefully) contributes to an efficient implementation of reactivity in R in the respect that binding functions are only executed when they actually need to.
As mentioned above, this might be unnecessary or even counter-productive in situations where the runtime of binding functions is negligible, but help in situations where unnecessary executions of binding functions is not desired due to their specific nature or long runtimes.
A second reason why the caching mechanism was implemented is to offer the possibility to specify bi-directional reactive bindings. AFAICT, you need some sort of caching mechanism in order to avoid infinite recursions.
See vignette Caching for details on this.
Caching is implemented by storing references of the "hidden parts" of an reactive object (the hidden instances of class ReactiveObject.S3
) in a registry that is an environment
and lives in getOption("reactr")$.registry
.
Ensuring example content in registry:
resetRegistry()
setReactive(id = "x_1", value = 10)
setReactive(id = "x_2", value = function() "object-ref: {id: x_1}")
registry <- getRegistry()
showRegistry()
The registry contains the UIDs of the reactive objects that have been set via setReactive
. See computeObjectUid()
for the details of the computation of object UIDs.
x_1_hidden <- getFromRegistry(id = "x_1")
x_2_hidden <- getFromRegistry(id = "x_2")
## Via UID //
getFromRegistry(computeObjectUid("x_1"))
getFromRegistry(computeObjectUid("x_2"))
This object corresponds to the otherwise "hidden part"" of x_1
that was implicitly created by the call to setReactive()
.
class(x_1_hidden)
ls(x_1_hidden)
## Some interesting fields //
x_1_hidden$.id
x_1_hidden$.where
x_1_hidden$.uid
x_1_hidden$.value
x_1_hidden$.hasPullReferences()
x_2_hidden$.id
x_2_hidden$.where
x_2_hidden$.uid
x_2_hidden$.value
x_2_hidden$.has_cached
x_2_hidden$.hasPullReferences()
ls(x_2_hidden$.refs_pull)
x_2_hidden$.refs_pull[[x_1_hidden$.uid]]
## Via ID (and `where`) //
rmFromRegistry(id = "x_1")
## --> notice that entry `2fc2e352f72008b90a112f096cd2d029` has been removed
## Via UID //
rmFromRegistry(computeObjectUid("x_2"))
## --> notice that entry `ab22808532ff42c87198461640612405` has been removed
showRegistry()
resetRegistry()
showRegistry()
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.