Skip to content

jankowtf/reactr

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

reactr

Reactive object bindings with built-in caching and push functionality

Installation

require("devtools")
devtools::install_github("Rappster/conditionr")
devtools::install_github("Rappster/yamlr")
devtools::install_github("Rappster/typr")
devtools::install_github("Rappster/reactr")
require("reactr")

Purpose

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).

Implementations

Two different reactivity implementations are provided:

  1. Implementation that builds on top of 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.

  1. Custom implementation (legacy but recommended for bi-directional reactive scenarios)

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.

Aknowledgements

The package is greatly inspired by reactivity as implemented by the shiny framework.

Vignettes (not refactored yet!)


Basics

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")

Highlighting selected features

  1. Strictness levels can be defined for
  • the creation process itself in setReactive() andsetShinyReactive(): see argument strict
  • getting the visible value of a reactive object: see argument strict_get
  • setting the visible value of a reactive object: see argument strict_set

See vignette Strictness for details.

  1. Propagation of changes: you can choose between a pull and a push paradigm with respect to how changes are propagated throughout the system.

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.

  1. Relations to shiny: as already mentioned, the package has a lot of relations to the shiny framework and thus the actual shiny package

Summary of the added functionality compared to what is currently offered by existing shiny functionality (shiny's limitations should always be read "AFAIK" ;-)):

  1. The same function can be used for setting both reative sources and observers.

  2. 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`.
  1. 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.

  1. Reference specification (only relevant for 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.:

  1. Via a function argument refs.
  2. 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.

  1. Caching mechanism (only relevant for 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.


Feature showcase: typed sources

## 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))

Feature showcase 2: pushing

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")

Feature showcase 3: getting closer to an actual use case

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")

Feature showcase 4: R6 and Reference Classes

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

Feature showcase 4: setReactive() (legacy)

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")

Unsetting reactive objects

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

NOTE

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.

Removing reactive objects

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)

Reactivity scenarios in detail

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())

Scenario 1: one-directional (1)

Scenario explanation

  • Type/Direction:

    A references B

  • Binding/Relationship:

    A uses value of B "as is", i.e. value of A identical to value of B

Example

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")

Scenario 2: one-directional (2)

Scenario explanation

  • Type/Direction:

    A references B

  • Binding/Relationship:

    A transforms value of B , i.e. value of A is the result of applying a function on the value of B

Example

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")

Scenario 3: one-directional (3)

Scenario explanation

  • Type/Direction:

    A references B and C, B references C

  • Binding/Relationship:

    A transforms value of B , i.e. value of A is the result of applying a function on the value of B

Example

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")

Scenario 4: bi-directional (1)

Scenario explanation

  • Type/Direction:

    A references B and B references A --> bidirectional binding type

  • Binding/Relationship:

    A uses value of B "as is" and B uses value of A "as is". This results in a steady state.

Example

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")

Scenario 5: bi-directional (2)

Scenario explanation

  • Type/Direction:

    A references B and B references A --> bidirectional binding type

  • Binding/Relationship:

    A uses transformed value of B and B uses transformed value of A.

    The binding functions used result in a steady state.

Example

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")

Scenario 6: bi-directional (3)

Scenario explanation

  • Type/Direction:

    A references B and B references A --> bidirectional binding type

  • Binding/Relationship:

    A uses transformed value of B and B uses transformed value of A.

    The binding functions used result in a non-steady state.

Example

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")

Caching mechanism (overview)

NOTE

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.

The registry

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.

Convenience functions

Ensuring example content in registry:

resetRegistry()
setReactive(id = "x_1", value = 10)
setReactive(id = "x_2", value = function() "object-ref: {id: x_1}")

Get the registry object

registry <- getRegistry()

Show registry content

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.

Retrieve from registry

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]]

Remove from registry

## 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

Reset registry

showRegistry()
resetRegistry()
showRegistry()