Type-safe monads and React

a match made in heaven

Giuseppe Maggiore
Hoppinger
12 min readSep 9, 2017

--

Introduction

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:

type Option<A> = { kind:"none" } | { kind:"some", v:A }

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 number or boolean(the < > brackets are suggestive in this regard) and Option gives us back the nullable definition of that type.

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

Option<number> = { kind:"none" } | { kind:"some", v:number }

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:

let unit = <A>(x:A) : M<A> => ...

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

let unit = <A>(x:A) : Option<A> => ({ kind:"some", v:x })

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 map function to transform a whole collection element-by-element.

One possible declaration of map is as follows:

let map = <A,B>(p:M<A>, f:(_:A)=>B) : M<B> => ...

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:

let map = <A,B>(p:Option<A>, f:(_:A)=>B) : Option<B> => 
p.kind == "none" ? ({ kind:"none" })
: ({ kind:"some", v:f(p.v) })

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:

let join = <A>(p:M<M<A>>) : M<A> => ...

In the case of Option, this leads us to:

let join = <A>(p:Option<Option<A>>) : Option<A> =>
p.kind == "none" || p.v.kind == "none" ? ({ kind:"none" })
: ({ kind:"some", v:p.v.v })

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):

let bind = <A,B>(p:M<A>, k:(_:A) => M<B>) : M<B> => {
let pk:M<M<B> = map(p, k)
return join(pk)
}

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:

let safe_div = (x:Option<number>, y:Option<number>) : 
Option<number> =>
bind(x, v_x =>
bind(y, v_y =>
v_y == 0 ? fail<number>()
: unit(v_x / v_y)))

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

bind(safe_div(a, b), x =>
bind(safe_div(c,d), y =>
...))

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 A which 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:

export type Cont<A> = (_:A) => void
export type C<A> = (cont:Cont<A>) => JSX.Element

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 output produced 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:

let unit = <A>(x:A) : C<A> => 
cont => cont(x) || null

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:

let map = <A,B>(p:C<A>, f:(_:A) => B) : C<B> =>
(cont_b:Cont<B>) => p (x => cont_b(f(x)))

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:

type JoinProps<A> = { p:C<C<A>>, cont:Cont<A> }
type JoinState<A> = { p_inner:"waiting"|JSX.Element, p_outer:JSX.Element }
class Join<A> extends React.Component<JoinProps<A>,JoinState<A>> {
constructor(props:JoinProps<A>,context:any) {
super()
this.state = { p_inner:"waiting", p_outer:props.p(p_inner =>
this.setState({...this.state,
p_inner:p_inner(x => props.cont(x))})) }
}
componentWillReceiveProps(new_props:JoinProps<A>) {
this.setState({ p_outer:new_props.p(p_inner =>
this.setState({...this.state,
p_inner:p_inner(x => new_props.cont((x))})) })
}
render() {
return <div>
{ this.state.p_outer }
{ this.state.p_inner == "waiting" ? null
: this.state.p_inner }
</div>
}
}

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

let repeat: <A>(key?: string, dbg?: () => string) => (p: (_: A) => C<A>) => (_: A) => C<A>

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 A as 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. repeat therefore 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:

let any: <A, B>(key?: string, className?: string, dbg?: () => string) => (ps: ((_: A) => C<B>)[]) => (_: A) => C<B>

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:

let retract: <A, B>(key?: string, dbg?: () => string) => (inb: (_: A) => B, out: (_: A) => (_: B) => A, p: (_: B) => C<B>) => (_: A) => C<A>

inb translates the input of the component to the desired type B, and out merges 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:

type Mode = "edit" | "view"
type EditToggleState = { mode:Mode, text:string }

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:

repeat<EditToggleState>("edit toggle sample")(
...
)({ mode:"edit", text:"" })

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:

repeat<EditToggleState>("edit toggle sample")(
any<EditToggleState, EditToggleState>()([
...,
...
]))({ mode:"edit", text:"" })

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:

repeat<EditToggleState>("edit toggle sample")(
any<EditToggleState, EditToggleState>()([
retract<EditToggleState, Mode>()(s => s.mode, s => m => ({...s, mode:m}),
mode => button<Mode>("Toggle editing")(mode == "view" ? "edit" : "view")
),
state =>
retract<EditToggleState, string>()(s => s.text, s => t => ({...s, text:t}),
rich_text(state.mode)
)(state)
])
)({ mode:"edit", text:"" })

The result is exactly as expected:

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.

--

--

Giuseppe Maggiore
Hoppinger

Giuseppe Maggiore holds a PhD in Computer Science, is CTO of Hoppinger, and teaches programming at Rotterdam University of Applied Sciences.