The power of the monadic react framework

In this article I will briefly introduce the concept of monad, discuss a new framework, monadic react, that encapsulates React inside a powerful monadic interface written in TypeScript, and then I will give a couple of examples of how to use this all in practice, and how powerful it proves to be.

 

Yet another introduction to monads

Monads are generic datatypes respecting a certain interface that encodes a given degree of “expressive power” of this datatype. There are many definitions of this interface, and they are all equivalent. The crucial aspect of a monad is that it can be:

– created
– transformed
– flattened

Since we said that at the core of a monad is a generic datatype, let us make an example of such a generic datatype:

An example of a generic datatype

The monad in this case will be just Option, without a specific argument. Option is a function from types to types: we pass it a type such as numberor boolean(the < > brackets are suggestive in this regard) and Optiongives us back the nullable definition of that type.

By substitution it is indeed easy to see that, according to the above definition of Option,

An example of code

is the return value of invoking Option with number as argument.

In general though, for a monad what we need is a generic type, which we shall call M.

Since we have no intention of knowing too much about M, we need to know how to construct an instance of M<A> from a simple object of type A which will become the payload of the constructed M<A>. This is a sort of external constructor, which in the design pattern literature would be called a factory, but in the monadic world is called unit (since it is the simplest non-empty instance of M we can think of, thus a sort of “one”).

The declaration of unit will therefore look like:

An example of code

A simple implementation for unit in the context of Option would become:

An example of code

that simply encapsulates the value x inside an Option container. Notice that not all definitions of M will be containers in the strict sense of Option, List, or Array, so the idea of x becoming the content of M<A> is a bit of a simplification, but bear with me for the moment.

The monad must be transformable, meaning that if we have an instance of a monad and a function that can process its input, then we want be able to transform the content of the monad and directly re-encapsulate it inside a new instance of a new monad. This is commonly known as map. In the last years we have seen this concept arise in many mainstream programming languages, as many generic collection libraries now feature the mapfunction to transform a whole collection element-by-element.

One possible declaration of map is as follows:

A possible declaration of a map

map takes as input an instance of the monad, p, “containing” values of type A. map also receives as input a function which can transform a value of type A into a new value of type B. The job of map is to find its way to each accessible element of type A inside p, apply f to it, and encapsulate the result(s) in a new instance of M. In the case of Option, this would become:

An example of code

The last point that distinguishes monads from other, simpler generic data types is that monads can be “joined” (also called “flattened” or “concatenated”). This means that an instance of a monad with another instance of the same monad inside itself can be converted to a single instance of the monad, thereby removing a containment level. This is a big deal, since it means that the monad is powerful enough to represent itself multiple times at once.

This leads us to the following signature:

An example of code

In the case of Option, this leads us to:

An example of code

It is also easy to see that, for lists, join will concatenate all the elements of a List<List<A>> into a single List<A>.

A very common utility that is usually defined on monads is therefore the bind operator that allows us to concatenate a monad, an operation that works on its content and produces new submonad(s) as result(s), and concatenates everything in a single resulting monad. Fortunately, such an operation is easily defined just in terms of map and join!

The definition of bind is therefore (split on multiple lines for explanatory reasons, given that it could easily just be written as a one liner):

An example of code

The interesting aspect of bind is that it makes it possible to combine monadic operations together. For example, we could define safe division with Option<number> in terms of:

An example of code

(where we assume we have already defined let fail = <A>() => ({ kind:"empty" })). Interestingly enough, since safe_div also returns an Option<number>, it can be used itself on the left-side of binding:

An example of code

 

React monad

Monads are powerful, and indeed we see them explicitly and implicitly finding their way in many libraries, often in disguised form. Examples of monads are the flatMap method of Immutablejs, the then method of Promise, and more. The flexibility of monads suggests (together with their host of widely used, existing implementations) that they can be used to simplify working in contexts ranging from graphics rendering, asynchronous computations, collection manipulation, IO, etc.

This led us to the question: “can React be encapsulated in a monadic interface” in order to remove some of its boilerplate and increase composability and safety?

The first step in order to answer such a question is the formulation of a generic data type encapsulating a monadic react component around an arbitrary type A. In what sense is a React component parameterized by a type? Let us take a step back and consider what a React component does: it is instantiated with some properties, one or multiple of which will often be a callback. The component will then render its contents (represented as a JSX.Element), perform its operations, and then eventually invoke its callback(s). The argument given to the callback is the only thing the rest of the application (the caller component) will ever see of the current component, meaning that the callback is invoked with a value of type Awhich represents the output of a component. This leads us to the realization that a React component which yields outputs of type A will take as input a property of type callback:(res:A) => void, which is a callback to which we pass the results of the component. Since this callback will trigger the rest of the dataflow dependent on the current component, we call it cont, short for continuation.

The signature of a monadic react component is therefore generalized to:

An example of code

C will be the type of our monadic React components, which will invoke cont whenever they have an output that they want to yield “to the rest of the program”.

Our C datatype clearly shows in what sense monads are not mere containers of data: the values of type A that will become the outputproduced by our component (for example, such a component might produce as outputs a string whenever the user types something) are passed to the continuation cont. The values are neither contained, nor saved in C, but rather dynamically computed and extracted on the fly. This makes C a sort of (potentially infinite) stream of A‘s.

The first step is to define a unit function which instantiates a component “around” a result. This is easier done than said:

An example of code

This component does not really need to perform any actual rendering, therefore it simply returns null right after passing the value x along to the continuation.

We can then easily build a map function for our component, which “simply” injects a fake continuation that will then translate the values with a transformation function:

An example of code

This implementation of map is attractively simple: we create an adapter component C<B> (a sort of placeholder) with its own callback, cont_b. The placeholder C<B> then just invokes p with a custom-made continuation, x => cont_b(f(x)), which is invoked by p with values x of type A. These values are transformed via f into values of type B, which are then passed along as final output to the continuation cont_b.

The final step is the join function which flattens two components nested inside each other into a single final component. join is slightly more difficult, as it requires caching the (renderable) result of the outer container in order to render it:

An example of code

Unfortunately, join must aggressively cache the previous results in order to “trick” React into not triggering too many spurious calls to cont (we consider a call to cont spurious when it is not performed as the result of an actual event, but rather because of the internal logic of React updates). This makes its implementation less pleasant to read.

The very last bit we need is a mechanism to instantiate a component inside a non-monadic React application. This is easier said than done: just invoke C<A> with a suitable callback, inside the render method of your component, and voilá! The library contains a component, called simple_application : <A>(p:C<A>, cont:Cont<A>) : JSX.Element, which does exactly this.

 

Combinators

In addition to the fundamental interface of monads, it is usually convenient to implement some extra operators that make working with one’s library easier. In the case of monadic react, there are three very important operators that encode some fundamental concepts to compose and transform existing components. We will begin by describing the general shape of these combinators, and then we will show them in action with concrete examples.

The first such operator is repeat. The signature of repeat is

An example of code

The (optional) key and dbg are parameters that respectively force a React key and print a debug message when the component is instantiated or updated. The real payload of repeat though is the function, p, which turns a value of type A into a monadic component which will produce another Aas output (C<A>). repeat will keep invoking p with its last own output, and repeat will also yield the output of p as if it were its own. repeattherefore encapsulates the notion of state in React as the closure of an ever-looping component. The reason why p has signature A => C<A> is also significant: think of a component such as an input box: it requires an initial value of type string, and produces (via onChange) outputs of type string. repeat expects such a component as p, in this case with signature string => C<string>.

Very often, multiple components are waiting for the user to interact with them, or for an api call to complete, but only the one that actually activates is of interest: the others can be ignored. In order to capture this pattern, we use the any combinator, which signature reads:

An example of code

any takes as input an array of components ps, each accepting an A as input and potentially producing a B as output. any will pass its own input of type A to each component in ps, and the first one to yield an output will see its output yielded by any as its own.

Sometimes, the various components we pass to any are not intrinsically built to all process the same A as input and the same B as output. More often than not, any works with a larger data structure than its elements. retract offers a way to automatically map the input and output of these components with two appropriate functions, so that all of the ps elements look like the same component. The signature of retract is therefore:

An example of code

inb translates the input of the component to the desired type B, and outmerges the output of the component with the previous input A in order to determine the new value of A.

 

Differences with actual implementation

It might be worth pointing out that the current implementation that you may download from npm differs in minor places from the one presented in the article: there are a few extra details such as additional callbacks, a global read-only context for shared utilities, etc. Moreover, the actual libraries contains tens of additional operators such as filter, button, rich_text, and much more (even routing).

Nevertheless, the core of the implementation and the associated concepts are exactly the same we just presented.

 

A concrete example of practical usage

Let us now see a small concrete example of usage of monadic react. Our goal is to build a rich text editor which can be toggled from editable to view-only by pressing a button.

The state of our component will contain the text written so far with the editor, plus whether or not we are editing or viewing:

An example of code

The core of our component will be a repeat encapsulating the idea that the EditToggleState is indeed our state, and as such is continuously fed back into the component itself:

An example of code

The first argument to repeat is the React key we want to set. It is always good practice to help React identify components quickly with a key, and our example makes no exception to this good practice.

The second argument is the core of our form, which we will specify in a moment. It will need to have signature EditToggleState => C<EditToggleState>.

The third argument is the initial state.

The core of our form will be made out of two separate sub-components: a button and a text editor. Since only one of them will be active at any given time, we will group them with any:

An example of code

The two arguments to any will be the button and the rich text editor. Unfortunately, but not unsurprisingly, these components do not manipulate the same types, and obviously neither of them is capable of manipulating EditToggleState directly. For this reason, they will both be encapsulated in a retract that converts EditToggleState back and forth from, respectively, the edit mode and the text:

An example of code

The result is exactly as expected:

An example of code

In about a dozen lines of code we have assembled a React form, with the added bonuses of enforced referential transparency and type safety at a higher level than when using React on its own.

 

See monadic react in action

If you are curious to see monadic react in action and play with it, you can head to the GitHub repository, the npm package, or the samples repository.

If you are wondering whether or not monadic react is ready for production, we believe the answer to be yes. The first website fully powered by monadic react is live and in use: GrandeOmega, featuring a full-blown editable CMS completely made in monadic react. The site is still in “startup mode”, meaning we are still working on plenty of things, but it clearly shows the promise and maturity of the framework. We believe we could not have set the site up so quickly and with so few bugs without the library.

Over de auteur

Guiseppe Maggiore is Digital Entrepreneur & CTO bij Hoppinger.

Mis niets

Schrijf je in voor onze nieuwsbrief en laat ons jouw gids zijn in een complexe digitale wereld.