I have read an excellent gentle introduction to reactive programming using RxJS library. The blog post is Getting Started With Rx.js: A Gentle Introduction by Jaime González García and the example is posted on jsFiddle for playing around with. It is embedded below; click on "Result" tab and click the "load more" button. It should load a new random twitter account on each click.
I liked this article and the example, it clearly shows the event processing steps; from clicking on the button to showing the result (even if I would prefer using something like virtual-dom to update the page efficiently).
Let us look at the example's code and see if we can "improve" it a little bit.
1 | let randomPlayer$ = Rx.Observable |
The refactorings I am going to do should serve as learning exercises, and might not be needed in this particular case. Yet they fight the whatever little boilerplate code exists even in this short program. I hate boilerplate and always look for ways to shorten and clarify every piece of software I write.
Functions instead of data
First, let us eliminate the duplicate GitHub API urls. We map every button click to the user list url string, and we also start the stream with the same url. Let us move the url into its own variable.
1 | const url = 'https://api.github.com/users' |
Look at the .map(_ => url)
step - it ignores the input arguments and just returns the same
value every time. Let us create a utility function that just returns the given value when called.
1 | const always = x => _ => x |
The function always
takes an argument value and returns another function. The returned function
does nothing but returns the original value when called. If you do not feel comfortable with
expressing the meaning of always
as a one liner (I often feel lost in one liners), write it
down explicitly.
1 | const always = x => _ => x |
Using helper always
we can replace hardcoded url
with clear code - we always map a click to
the url
. We can even refactor always
use and "hide" the url inside.
1 | const always = x => _ => x |
.map(...)
requires a function, thus we give it toUrl
, while .startWith(...)
needs an
actual value, thus we give it toUrl()
.
You can find the updated example at 52mwv7gn/1/ and below
I often return a function to access the data rather than the data in my code. This helps hide the implementation details as well as protects the data from mutations. For example instead of returning a user object and letting the outside code read its properties (and thus being able to modify the user object) I can return a function that allows reading a particular property but not setting it.
1 | // using objects directly |
1 | // returning a function to limit access to the object |
Shorten promise boilerplate
Let us shorten the next two steps - making an Ajax request for data and extracting the JSON from the response.
1 | .flatMap(url => Rx.Observable.fromPromise(fetch(url))) |
Notice that the argument name to both .flatMap
calls does not matter, it is only used for
clarity. Thus in reality this code has a very strong boilerplate smell. If we replace the argument
with letter "x" then 45 characters out of 50 in the two lines are the same!
1 | .flatMap(x => Rx.Observable.fromPromise(fetch(x))) |
Let us look at the first call's callback. This is the (almost) the simplest function composition example one can find:
1 | x => Rx.Observable.fromPromise(fetch(x)) |
Every time we see function f
applied to the result of calling another function g
to
argument x
we can replace f(g(...))
with equivalent composed function
1 | // assume we have "compose" helper function |
Writing the "compose" helper fuction in this case is very simple - we are ignoring context and only assume a single argument
1 | function compose(f, g) { |
Now let us replace first .flatMap
with shorter equivalent code. We can even just provide
the callback without explicit url
parameter, achieving
the pointfree programming style
1 | const ajax = compose(Rx.Observable.fromPromise, fetch) |
You can try the code at jsfiddle.net/h75k3y8z/1/ or below
Let us simplify the second .flatMap
callback. Instead of simple composition f(g(x))
it has a method call on the argument f(x.g())
. We need to compose f
with invoking a method
on a object. We first know the name of the method json
but no the object yet - the object
will be passed to the callback. Let us write simple helper function to invoke a method given
just its name on an object passed later.
1 | function method(name) { |
method('email')
just creates a function that is waiting for an actual object. In our RxJS
example we need a function that will invoke json()
on the given response object.
1 | .flatMap(x => Rx.Observable.fromPromise(x.json())) |
Now we have simple function composition again with first function being Rx.Observable.fromPromise
and the second function being json
function. Applying compose again makes the callback go away.
1 | const ajax = compose(Rx.Observable.fromPromise, fetch) |
The example still works like before, we just replaced the code with equivalent shorter code
Using external library
We have only introduced 3 helper functions above
1 | const always = x => _ => x |
The functions are so short it is hard to make mistakes in them ;) Yet if you start using little helper functions like these you immediately will want more of them. You will want more helpers for different situations (like grabbing the property of an object instead of calling a method), and more power in each helper (like composing more than two functions or passing arguments to the method call). Writing these helpers and testing them inside each function quickly becomes a chore. Luckily there are entire libraries of these functions, lodash and Ramda being my favorite.
For the example above, Ramda has everything we need right out of the "box". Let us add the library
to the fiddle (using CDN) and replace our custom code with Ramda's functions. The syntax almost
maps one to one, except for method
helper - Ramda needs to know the number of arguments
expected by the method, thus we pass 0
first.
1 | const toUrl = R.always('https://api.github.com/users') |
We run the new code ... and it does not work. The browser console shows an error
1 | Uncaught TypeError: Failed to execute 'fetch' on 'Window': parameter 2 ('init') is not an object. |
What is this? Well, R.compose
is more powerful than our custom code before; it passes
all arguments along. For example, if we create a little wrapper function to see what arguments
are passed to fetch
(which is the new built-in HTML5 API function) we can see those.
1 | const ajax = R.compose(Rx.Observable.fromPromise, function (url) { |
Run the code again and see in the browser console the following
1 | ["https://api.github.com/users", 1, FlatMapObservable] |
Just like Array.map
, Observable.flatMap
passes to each callback the item, the index and
the "list" (in this case Observable instance) itself. When using arrays, this leads to
some madness
(and an excellent interview question).
In our case, fetch
gets extra arguments and breaks. But does not fetch
only expect a single
argument? After all, if we print fetch.length
we get 1! Not really, the
fetch API
allows additional options object as the second argument
1 | fetch(url, { |
Thus fetch
is not ignoring the second argument and breaks if the argument is an iteration
index for example. We should really tell fetch
to ignore everything but the first argument -
we should make fetch
unary. We can write a little helper ourselves easily or use Ramda's
function
1 | function unary(f) { |
Now the code works as expected!
We often have to do this function signature "massaging" when adapting the expected arguments to what is really passed into the function.
Compose promises instead of flap mapping
We have two chained steps to .flatMap
in our stream, and they deal with a single request:
making an ajax request (returns a promise) and then extracting JSON result (also returns a
promise in the new HTML5 API). We can combine the promises using .then()
and use a .flatMap
instead.
1 | const ajaxP = R.unary(fetch) |
Even better, Rx.Observable.flatMap
works directly with a promise object, without mapping into
Observable first
1 | const ajaxP = R.unary(fetch) |
Finally, we have a promise-returning function ajaxP
chained to jsonP
. The result
of ajaxP
is passed to jsonP
and then the result is returned (inside the resolved promise).
This is very similar to the function composition, only the passing the result along is
different. Instad of simplifying f(g(x))
into compose(f, g)(x)
promise composition combines the promise-returning functions using .then
method like
this f(x).then(g) = composeP(f, g)(x)
Ramda has promise composition under R.composeP
1 | const ajaxP = R.unary(fetch) |
The promise composition works from right to left (first ajax, then json). Our original code
was easier to read left to right since it was ajaxP(url).then(jsonP)
. There is an equivalent
helper method R.pipeP which is just like compose
but expects
its arguments in left to right order. I find it usually easier to read "piped" code
than "composed" code. Both produce the same result
Conclusion
Functional reactive programming is all about using little helpers to get rid of unnecessary code in your reactive streams. Writing your own helper functions is a great exercise and a perfect first step to shorter and clearer code. Then switch to using a dedicated library and get rid of most of the code in your application.