-
Notifications
You must be signed in to change notification settings - Fork 566
/
reactivity-motivation.Rmd
230 lines (165 loc) · 11.4 KB
/
reactivity-motivation.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# Why reactivity? {#reactive-motivation}
```{r setup, include=FALSE}
source("common.R")
```
## Introduction
The initial impression of Shiny is often that it's "magic".
Magic is great when you get started because you can make simple apps very very quickly.
But magic in software usually leads to disillusionment: without a solid mental model, it's extremely difficult to predict how the software will act when you venture beyond the borders of its demos and examples.
And when things don't go the way you expect, debugging is almost impossible.
Fortunately, Shiny is "good" magic.
As Tom Dale said of his Ember.js JavaScript framework: "We do a lot of magic, but it's *good magic*, which means it decomposes into sane primitives." This is the quality that the Shiny team aspires to for Shiny, especially when it comes to reactive programming.
When you peel back the layers of reactive programming, you won't find a pile of heuristics, special cases, and hacks; instead you'll find a clever, but ultimately fairly straightforward mechanism.
Once you've formed an accurate mental model of reactivity, you'll see that there's nothing up Shiny's sleeves: the magic comes from simple concepts combined in consistent ways.
In this chapter, we'll motivate reactive programming by trying to do without it and then give a brief history of reactivity as it pertains to Shiny.
## Why do we need reactive programming? {#motivation}
Reactive programming is a style of programming that focuses on values that change over time, and calculations and actions that depend on those values.
Reactivity is important for Shiny apps because they're interactive: users change input controls (dragging sliders, typing in textboxes, checking checkboxes, ...) which causes logic to run on the server (reading CSVs, subsetting data, fitting models, ...) ultimately resulting in outputs updating (plots redrawing, tables updating, ...).
This is quite different from most R code, which typically deals with fairly static data.
For Shiny apps to be maximally useful, we need reactive expressions and outputs to update if and only if their inputs change.
We want outputs to stay in sync with inputs, while ensuring that we never do more work than necessary.
To see why reactivity is so helpful here, we'll take a stab at solving a simple problem without reactivity.
### Why can't you use variables?
In one sense, you already know how to handle "values that change over time": they're called "variables".
Variables in R represent values and they can change over time, but they're not designed to help you when they change.
Take this simple example of converting a temperature from Celsius to Fahrenheit:
```{r}
temp_c <- 10
temp_f <- (temp_c * 9 / 5) + 32
temp_f
```
So far so good: the `temp_c` variable has the value `r temp_c`, the `temp_f` variable has the value `r temp_f`, and we can change `temp_c`:
```{r}
temp_c <- 30
```
But changing `temp_c` does not affect `temp_f`:
```{r}
temp_f
```
Variables can change over time, but they never change automatically.
### What about functions?
You could instead attack this problem with a function:
```{r}
temp_c <- 10
temp_f <- function() {
message("Converting")
(temp_c * 9 / 5) + 32
}
temp_f()
```
(This is a slightly weird function because it doesn't have any arguments, instead accessing `temp_c` from its enclosing environment[^reactivity-motivation-1], but it's perfectly valid R code.)
[^reactivity-motivation-1]: R uses "lexical scoping" for looking up the values associated with variable names.
You can learn more about it in <https://adv-r.hadley.nz/functions.html#lexical-scoping>.
This solves the first problem that reactivity is trying to solve: whenever you access `temp_f()` you get the latest computation:
```{r}
temp_c <- -3
temp_f()
```
It doesn't, however, minimise computation.
Every time you call `temp_f()` it recomputes, even if `temp_c` hasn't changed:
```{r}
temp_f()
```
Computation is cheap in this trivial example, so needlessly repeating it isn't a big deal, but it's still unnecessary: if the inputs haven't changed, why do we need to recompute the output?
### Event-driven programming {#event-driven}
Since neither variables nor functions work, we need to create something new.
In previous decades, we would've jumped directly to *event-driven programming*.
Event-driven programming is an appealingly simple paradigm: you register callback functions that will be executed in response to events.
We could implement a very simple event-driven toolkit using R6, as in the example below.
Here we define a `DynamicValue` that has three important methods: `get()` and `set()` to access and change the underlying value, and `onUpdate()` to register code to run whenever the value is modified.
If you're not familiar with R6, don't worry about the details, and instead focus on following examples.
```{r}
DynamicValue <- R6::R6Class("DynamicValue", list(
value = NULL,
on_update = NULL,
get = function() self$value,
set = function(value) {
self$value <- value
if (!is.null(self$on_update))
self$on_update(value)
invisible(self)
},
onUpdate = function(on_update) {
self$on_update <- on_update
invisible(self)
}
))
```
So if Shiny had been invented five years earlier, it might have looked more like this, where `temp_c` uses `<<-`[^reactivity-motivation-2] to update `temp_f` whenever needed.
[^reactivity-motivation-2]: `<<-` is called the super-assignment operator, and here it modifies `temp_f` in the global environment, rather than creating a new `temp_f` variable inside the function as `<-` would.
You can learn more about `<<-` in <https://adv-r.hadley.nz/environments.html#super-assignment-->.
```{r}
temp_c <- DynamicValue$new()
temp_c$onUpdate(function(value) {
message("Converting")
temp_f <<- (value * 9 / 5) + 32
})
temp_c$set(10)
temp_f
temp_c$set(-3)
temp_f
```
Event-driven programming solves the problem of unnecessary computation, but it creates a new problem: you have to carefully track which inputs affect which computations.
Before long, you start to trade off correctness (just update everything whenever anything changes) against performance (try to update only the necessary parts, and hope that you didn't miss any edge cases) because it's so difficult to do both.
### Reactive programming
Reactive programming elegantly solves both problems by combining features of the solutions above.
Now we can show you some real Shiny code, using a special Shiny mode, `reactiveConsole(TRUE)`, that makes it possible to experiment with reactivity directly in the console.
```{r, cache = FALSE}
library(shiny)
reactiveConsole(TRUE)
```
As with event-driven programming, we need some way to indicate that we have a special type of variable.
In Shiny, we create a **reactive value** with `reactiveVal()`.
A reactive value has special syntax[^reactivity-motivation-3] for getting its value (calling it like a zero-argument function) and setting its value (set its value by calling it like a one-argument function).
[^reactivity-motivation-3]: If you happen to have ever used R's active bindings, you might notice that the syntax is very similar.
This is not a coincidence.
```{r}
temp_c <- reactiveVal(10) # create
temp_c() # get
temp_c(20) # set
temp_c() # get
```
Now we can create a reactive expression that depends on this value:
```{r}
temp_f <- reactive({
message("Converting")
(temp_c() * 9 / 5) + 32
})
temp_f()
```
As you've learned when creating apps, a reactive expression automatically tracks all of its dependencies.
So that later, if `temp_c` changes, `temp_f` will automatically update:
```{r}
temp_c(-3)
temp_c(-10)
temp_f()
```
But if `temp_c()` hasn't changed, then `temp_f()` doesn't need to recompute[^reactivity-motivation-4], and can just be retrieved from the cache:
[^reactivity-motivation-4]: You can tell it doesn't re-compute because "Converting" is not printed.
```{r}
temp_f()
```
A reactive expression has two important properties:
- It's **lazy**: it doesn't do any work until it's called.
- It's **cached**: it doesn't do any work the second and subsequent times it's called because it caches the previous result.
We'll come back to these important properties in Chapter \@ref(reactive-graph).
## A brief history of reactive programming
If you want to learn more about reactive programming in other languages, a little history might be helpful.
You can see the genesis of reactive programming over 40 years ago in [VisiCalc](https://en.wikipedia.org/wiki/VisiCalc), the first spreadsheet:
> I imagined a magic blackboard that if you erased one number and wrote a new thing in, all of the other numbers would automatically change, like word processing with numbers.\
> --- [Dan Bricklin](https://youtu.be/YDvbDiJZpy0)
Spreadsheets are closely related to reactive programming: you declare the relationship between cells using formulas, and when one cell changes, all of its dependencies automatically update.
So you've probably already done a bunch of reactive programming without knowing it!
While the ideas of reactivity have been around for a long time, it wasn't until the late 1990s that they were seriously studied in academic computer science.
Research in reactive programming was kicked off by FRAN [@fran], **f**unctional **r**eactive **an**imation, a novel system for incorporating changes over time and user input into a functional programming language. This spawned a rich literature [@rp-survey], but had little impact on the practice of programming.
It wasn't until the 2010s that reactive programming roared into the programming mainstream through the fast-paced world of JavaScript UI frameworks.
Pioneering frameworks like [Knockout](https://knockoutjs.com/), [Ember](https://emberjs.com/), and [Meteor](https://www.meteor.com) (Joe Cheng's personal inspiration for Shiny) demonstrated that reactive programming could make UI programming dramatically easier.
Within a few short years, reactive programming has come to dominate web programming through hugely popular frameworks like [React](https://reactjs.org), [Vue.js](https://vuejs.org), and [Angular](https://angularjs.org), which are all either inherently reactive or designed to work hand-in-hand with reactive back ends.
It's worth bearing in mind that "reactive programming" is a fairly general term.
While all reactive programming libraries, frameworks, and languages are broadly concerned with writing programs that respond to changing values, they vary enormously in their terminology, designs, and implementations.
In this book, whenever we refer to "reactive programming", we are referring specifically to reactive programming as implemented in Shiny.
So if you read material about reactive programming that isn't specifically about Shiny, it's unlikely that those concepts or even terminology will be relevant to writing Shiny apps.
For readers who do have some experience with other reactive programming frameworks, Shiny's approach is similar to [Meteor](https://www.meteor.com/) and [MobX](https://mobx.js.org/), and very different than the [ReactiveX](http://reactivex.io/) family or anything that labels itself Functional Reactive Programming.
## Summary
Now that you understand why reactive programming is needed and have learned a little bit of history, the next chapter will discuss more details of the underlying theory.
Most importantly, you'll solidify your understanding of the reactive graph, which connects reactive values, reactive expressions, and observers, and controls exactly what is run when.