Once in a while someone points out how ReactiveX (Rx) does not have means to avoid glitches. [1] [2]
Glitches are temporary inconsistencies emitted by Observables. Consider this example:
Events in parentheses happen “simultaneously”. In practice they happen at slightly different times, but separated by only a couple of nanoseconds, so people understand them to be simultaneous. Events (c1c2)
are called glitches and sometimes considered a problem because one would expect only c2
to happen.
The emphasis people put on glitches and their problems is usually exaggerated.
There is a style of writing Rx code that allows you to be sure that glitches either don’t happen or don’t create problems in your real-world application.
When do glitches happen? Usually glitches are demonstrated by giving the classical diamond case. An Observable A is transformed into Observables B and C, then those are combined to create Observable D:
Usually the complaint revolves around the lack of operators to support the desired outcome. combineLatest
falls short. zip
has its problems, too. There actually isn’t any operator that we can use to replace combineLatest in the example above. That would require, under the hood, a transaction manager. This is what libraries like derivablejs and menrva do.
However, the diamond example is contrived. If you know you want to produce Observable d
out of b
and c
, you can just build d
directly based on a
, instead of going through b
and c
:
That said, there are non-contrived cases in real applications where glitches do appear and it doesn’t make sense to refactor code like we did above. For instance, maybe b
and c
depend on other Observables than just a
. Take the case where we want to send an analytics report whenever an error happens, reporting also what was the last action the user took before that error occurred.
The combineLatest we used for this creates glitches and also other undesired analytics messages as result. For example:
The error e2
was immediately caused by u2
, yet the combineLatest produced an undesired event e1u2
. Also, e2u3
is caused by u3
alone, which is semantically wrong for analyticsMessages
because error e2
did not happen after user action u3
.
What we should have done instead is ask ourselves these questions about two orthogonal concerns: what does analyticsMessages depend on and when does analyticsMessages emit events? Both errors
and userActions
are the answer to the what question, but only errors
is the answer to the when question. So we need an operator that combines the values of multiple Observables only when a specific Observable emits. That one is withLatestFrom
:
Which behaves as:
There was no inherent problem with combineLatest
. It simply does what it promises to do and it has its legitimate use cases. It defers from withLatestFrom
simply with regards to when the result should emit.
a.combineLatest(b, c, fn)
: emits when any one of a
, b
, or c
emit.a.withLatestFrom(b, c, fn)
: emits only when a
emits.Over time, I have developed two rules of thumb to help me choose which combination operator I want:
With combineLatest
, people sometimes complain about coincidental events being joined. For instance look at this case:
The events height = 177
and weight = 78
happen “at the same time”, yet, combineLatest
produces two events: bmi = 23
and bmi = 25
. We followed our rule of thumb because height and weight are usually completely independent concepts or values evolving over time. Typically, this case is considered a glitch, but I contend it’s a harmless glitch.
For UI purposes, if we are displaying the evolving BMI value, we won’t see 23
because it will be immediately replaced by 25
. For calculations and logging, it is harmless to report bmi = 23
and bmi = 25
consecutively. To us, those events happened “at the same time”, but for the computer and for calculations, they actually happened sequentially. If any other part of your application which depends on the bmi
Observable happens to be faulty because of this harmless glitch, then that’s a symptom of buggy implementation elsewhere, not with bmi
. As far as bmi
Observable is concerned, it is completely correct.
Programming with Rx is peculiar in that we do not see glitches or the problems of glitches as long as we solve problems in an idiomatic Rx style. For instance, using an imperative API like set()
is frowned upon for Rx code. Libraries that support set()
for observables such as derivablejs and menrva need to have a transaction manager. Because transactions are able to control when do changes from set()
calls get committed, they provide a way of imperatively controlling the when concern. In Rx code, we reactively control the when concern by choosing the appropriate operator.
Imperative techniques in Rx such as usage of Subject
and its onNext()
calls are frowned upon because you cannot separately control when the Subject emits. A second downside of imperative updates is they make the what concern implicit: it depends on what is the context or module containing the code for the imperative set()
call. Reactive techniques make both when and what explicit and declared in a single statement. This reason is just one among many which makes me believe the reactive paradigm should be our default choice.
Copyright (C) 2015 Andre 'Staltz' Medeiros, licensed under Creative Commons BY-NC 4.0, translations to other languages allowed.