In this two-part series, I want to take a functional approach to building React applications.
There will be mathematical theory sprinkled throughout the series, and hopefully by the end of it you will pick up some useful techniques!
Series contents:
- Part 1 - Deconstructing the React Component
- Part 2 - The Reader monad and read-only context
In the previous post, we looked at React
through a more functional lens and rebuilt parts of the React component by using a View
box that contains the computation Props -> Element
.
There are two remaining React component API that I want to replicate: context and state. And in this post we will learn how to add a read-only context for our application using yet another box object.
So onwards and upwards!
Motivation behind context
Say that we have a copyright notice View as follows.
In order for this View to be used in our application, we will need to pass the props author
and year
down from the root View.
As the number of props grow in the application, the more data we will have to manually pass down. Not only is this a tedious process, but it also means that the root View will need to take in every single prop as its own input just so they can be passed down to descendents.
But wait, there is a better way!
Meet the Reader
Let’s start by defining a new Reader box that contains a computation of the type Context -> a
. What is a
here?
It really could be anything, but for now we can focus on computations that return a View.
The Reader defines one function runReader
that takes in the value of the context, and returns the result of the
computation.
Now, we can use the Reader like this.
Here, the Reader’s computation returns a View, which is our previously defined copyrightNotice
,
that has its input props contramapped from the Reader context. So, when we run the reader with a context value,
we get the value (View) out of the box, which we can fold down to get the final element.
Take a moment to let that sink in.
Now, just like we did with View previously, we can make the Reader a pointed functor.
Then, we can use map
to wrap the notice with a <footer>
tag.
But what else can Reader hold?
Remember that the Reader contains the computation Context -> a
, where a
can be anything?
So far we’ve returned a View from the computation, but it can really be any value, even a function!
Let’s look at a function that changes the color of an element within a View.
We can wrap this up in a Reader.
To use this Reader, we need to run it with a context, then apply it to an element in the View.
Rather tediously, we have to run both readers before can apply the colorize
function to the footer view.
Of course, there is a better way to do this. And that is with the ap
function.
The ap
of a Reader will call its resulting function with the resulting value of another
Reader. We can visualize this like so.
This means that we can simplify our usage of withColor
to this.
A bit of theory: The Reader is now an Applicative in addition to being a functor.
An Applicative A
must obey the following laws.
- Identity:
A.of(x => x).ap(v) === v
- Homomorphism:
A.of(f).ap(A.of(x)) === A.of(f(x))
- Interchange:
u.ap(A.of(y)) === A.of(f => f(y)).ap(u)
For the Reader just substitute A
with Reader
in the above laws.
We can verify that these laws do indeed hold in this test.
Chaining the Reader context
Let’s consider another view for displaying the page title.
Now, say we want to read the title
from the context, just like we did for the copyright notice.
But, how can we use both the header
and footer2
together? Maybe we map
over the View inside
the header
?
This solution works, but isn’t ideal since running combined.runReader
returns yet another Reader.
Therefore, in order to get the resulting element out we will have to run the Reader twice!
Imagine having to use invoke runReader
once for each usage of the context. Such drudgery! Luckily
for us, there is a much better way to do this. We need to add the chain
function.
The chain
function takes in a function f
, similar to map
. However, f
for chain
is a function that takes the result from the previous computation as the input, then
returns another Reader from it. And when the resulting Reader is run, the context will be
passed to all of the chained computations.
This allows us to easily combine the header and footer boxes.
Indeed, we now only need to run the Reader once.
A bit of theory: The Reader is now a Monad.
A Monad M
must obey the following laws.
- Left identity:
M.of(a).chain(f) === f(a)
- Right identity:
m.chain(M.of) === m
- Associativity:
m.chain(f).chain(g) === m.chain(x => f(x).chain(g))
The M
in our case is the Reader.
We can verify that these laws do indeed hold in this test.
But what is a monad exactly?
You can think of a monad as any object that implements the chain
function, and obeys the monad laws.
Note that you may see bind
or flatMap
being used instead of chain
in other libraries or articles. They are usually
the same as chain
, and can be treated as aliases. You might also note a similarity between chain
and the Promise
then
method. They are indeed almost the same. However, in the case of Promises, the computation is not lazy, and has
an immediate effect when constructed, whereas the Reader does nothing until runReader
is called.
Why didn’t you just ask?
Let’s define a helper function for our Reader monad.
Now, we can start the chain by asking for the context.
We will also add a helper for this dot-chain sytle of code using generator functions.
The Monad.do
function allows us to write “normal-looking” JavaScript code.
Assembling all the pieces
Recall that the app from the previous post rendered a message along with some header and footer content.
Let’s use our new friend, the Reader monad, and provide some internationalization (i18n) support for that “Hello ___!” message.
We’ll start by defining a message bundle and a formatMessage
function.
Now, we can create context-aware functions using formatMessage
.
Also, recall our three morpshisms on elements from the last post. We’ll use them in the app as well!
Finally, we can build our entire app as follows.
While we’re at it, let’s crank it up to eleven by styling the app.
When we render the app, this will appear on the screen.
The runnable source code for the full example can be found in this repository.
Summary
In this post, we learned about Applicatives and Monads and created the Reader to provide read-only context for our application.
In the next post we will wrap up this series by adding support for stateful computations. So stay tuned!
If you want additional resources on Monads, I recommend these.