This episode is freely available thanks to the support of our subscribers
Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members.Become a Subscriber →
We look at an example of a reactive pipeline with surprising behavior, discuss why it occurs, and how it could be improved.
00:06 Today we'd like to show a specific problem with reactive programming
that comes up in practice a lot. We'll discuss why it happens and what we can do
about it.
Constructing an Example
00:31 Let's say we're building an imaginary settings app that has
switches for airplane mode, Wi-Fi, and cellular data. When airplane mode is
enabled, Wi-Fi and cellular have to be disabled. When airplane mode is disabled,
we have to take the two original settings for the Wi-Fi and cellular switches.
Finally, we want to keep track of when both Wi-Fi and cellular are enabled.
01:05 We start to build the logic using a minimal reactive library we
wrote:
03:02 Ideally, we would've used a function like combineLatest in the
above snippet, but our reactive library only offers map and flatMap, which
also works.
03:14 We observe the cellularEnabled property and print its value. If
we turn on airplane mode, we get a false for cellularEnabled:
04:41 Now we can observe the latter property to see if both Wi-Fi and
cellular are enabled. We print some dashes to make clear what's happening — we
start out with true for wifiAndCellular, and then we enable airplane mode
and we get false twice:
05:13 The observer of wifiAndCellular gets called twice, because the
new value travels over two paths, via wifiEnabled and cellularEnabled. This
is an interesting effect, but it's not yet the problem we want to show. If we
disable airplane mode again, we see that the observer first gets called with
true and then with false:
06:05 The observer getting called with two different values is
surprising, but it happens for the same reason as before: the value traveling
down two different paths and rejoining in the end. The following diagram shows
us what's happening:
06:27 Here we see the dependencies of all properties. By sending a new
value to airplaneMode, its children get triggered. After notAirplaneMode,
the value travels down the left branch to our observer, wifiAndCellular, at
which point the value for cellularEnabled, in the right branch, hasn't yet
been updated.
07:44 After the observer is called and we print the first output,
notAirplaneMode continues with its other children, and the value travels down
the right branch to our observer, which now prints the correct value:
Problems with Reactive Glitches
08:04 In practice, this weird behavior doesn't have to be problematic.
If we take a value from a reactive pipeline and bind it to a label, we may end
up setting a wrong value briefly before we set the correct value. This isn't the
most efficient, but it still works. However, if we do something else with
received values, like start a network request, write to a file, or append to an
array, then it's definitely problematic that we receive multiple, intermediary
values instead of a single final result. Some of those intermediary results can
even be the wrong value, leading to unexpected behaviors and bugs.
08:45 It's up to the developer to choose the right combinators for their
graph to mitigate unwanted effects. In this case, we should've used a function
like zip — where we join the paths together — instead of flatMap, in order
to wait for the values from both branches before continuing.
09:24 Before we go to the solution, let's look at another abstraction
problem. Our three uses of flatMap are sort of a reactive version of &&. We
could write a function that combines observable Booleans this way:
10:20 We'd like to write this function and use it, but it's unfortunate
that we're calling flatMap in its implementation. We should really leave this
choice to the developer who uses the framework. This is because, in some cases,
they might not want to use flatMap, but rather zip, as in our current
example.
Topological Sorting
10:45 A modified version of our reactive library offers a solution. We
copy our sample code into another playground with the updated library. The
output is very different, because this library processes the observed values
differently. Now we only get out a single printed value each time we send a new
value in:
true
–––
false
–––
true
11:37 To understand what the library does, we look at the diagram again.
We reorganize the graph in different levels of height (or depth). The bottom of
the graph is height zero, and with each level up, the nodes get a larger number:
12:41 What happens when we send a new value to airplaneMode is
visualized in the animation below. Here's the gist of it: sending a new value to
airplaneMode places all of its children in a queue. airplaneMode only has
one child, and after triggering it, we move on to notAirplaneMode, which has
two children. Both are put in the queue, sorted by height. In this case, both
wifiEnabled and cellularEnabled have the same height, so it doesn't matter
which is processed first. After processing wifiEnabled, we put its child (the
map operation) in the queue. Now we have two elements with different heights
in the queue, and because cellularEnabled has a higher number, we process it
first. This puts its child in the queue, resulting in two items with the same
height again, so we continue with either one of them. When we process the first
one, its child, cellularAndWifi, is put in the queue, and after the second
.map { ... } node is processed, we don't put its child in the queue because
it's already there. Finally, the observer is queued and processed:
15:13 In short, the values flow through the graph in another way; they
trickle down the different branches more synchronously, in an order you'd expect
from a reactive framework. We now don't have to make a distinction between
flatMap and zip, since the framework triggers all observers in the correct
order.
Pros and Cons
16:12 We can now use the && operator for every step, including the
last one where we would've had to use zip in the previous version of the
framework:
16:51 Using the queue algorithm, we don't have to think about how to
tie together our reactive properties, and we can use operators like &&. The
tradeoff is that the internal processing of the queue has become more complex.
17:40 This algorithm is called a topological sort, because it sorts the
graph's nodes in a topological way in order to process them in the correct
order. Stick around for an upcoming episode in which we want to actually
implement this algorithm, starting out with the first version of the library we
showed.