Reducers are Monoids on Functions
Reducers — a tool for handling state changes — help engineers manage complexity in their applications. In this post, we'll dig into how these reducers tick by exploring some monoids on functions, learning some formal terms, and discovering the underlying reason that many engineers reach for reducers to simplify mutations of state. All code examples will be presented in OCaml, TypeScript, Haskell, and Swift.
Preliminaries
I expect readers to be familiar with the material covered in the first post on semigroups and monoids, but to keep this post more or less self-contained I'll review the declaration of semigroups and monoids here:
Endofunctions are monoids
Endofunctions, or in source code, Endo
, are functions from some type to itself; for example int -> int
or Person -> Person
. Endofunctions come up in everyday programming as a mechanism for somewhat paradoxicly describing mutations to data immutably. In other words, we can reify mutations into values.
To see this, let's first implement endofunctions:
And then consider a Person
object/record/struct
A person, Fred, can age one year like so:
Notice a few interesting facts here:
oneYearOlder
, the change, is a value that we can store, manipulate, and do with what we choose.- We were able to change
fred
despitefred
being an immutable value in our language. To do this we can introduce a new value with the changes applied.
The Semigroup
The semigroup instance on endofunctions gives us a way to combine two changes into a single change. Concretely, we may want to also change our name field to add a last name, for example, we want to addLastNameSmith
:
The Monoid
The monoid instance provides a way to model a trivial change. With this addition, we now have a nice base case if we're building up an arbitrary amount of changes based on some runtime information:
We can further improve on the above by utilizing a writer monad as described in an older post to remove all of the boilerplate doing something like the above.
The Instance
An astute reader may notice "combining changes" is function composition or , and the trivial change is the identity function or . Thus the monoid we've been talking about this whole time is .
Pointwise operations over monoids are monoids
A pointwise operation is some operation on some type that is "lifted" to act on functions that return . More formally, . If code is your thing, what follows is the function that lifts a binary operation pointwise:
An interesting property of pointwise operations is that if the underlying operation is a monoid then the resulting pointwise operation is a monoid too! I think a nice proof of this is to show the source code that performs this operation for us.
Sometimes you want to manipulate functions over the operations rather than the underlying operations themselves. The nice thing is we don't have to give up our monoidal superpowers when we do so!
Reducers
Manipulation of state in large applications quickly gets hairy. As an application grows, it becomes a real challenge to be sure that state mutations affect only the components you want it to. One mitigation is to centralize all of your state manipulation as best as you can — ideally to one function or one file or one module. To do so, we can decouple an intention to change state (or an action) from the actual state change itself.
Reducers are one way to cleanly declare atomic chunks of state manipulation in response to these actions. Smaller reducers can be composed into bigger ones as our application's state management grows in scope.
Let's take Redux reducers as an example to explore further. According to the official documentation, a reducer is defined by a function from the previous state and an action into a new state . In theory, we would feed some library a bunch of these reducers and in our application we could fire actions to trigger these state changes. In code, this definition of a reducer looks as follows:
In Redux, reducers can be combined with a combineReducers
function, but it's not immediately obvious exactly how this function would work.
Reducers are monoids
Redux is great because it introduced the concept of reducers to the masses. But instead of using a library directly, let's re-arrange Redux's reducer function a bit to see if we can build the library ourselves.
And we have a monoid! You could say a reducer is just the monoid lifted pointwise over actions. The monoid is precisely why using reducers is a nice way to decompose and reason about state changes in your application: Breaking down problems into pieces makes them more manageable, and the identity and associativity of the monoid means gluing them back together is easy. In fact, with our monoid instance on the manipulated reducer the and give us a Redux-like library for free.
Conclusion
Endofunctions are monoids, pointwise monoidal operations are monoids, and combining these two function-monoids give us reducers! The monoidal formulation of reducers obsoletes a need for a library to provide us a way to combine reducers and give us motivation for why reducers are such a nice way to manage changes to a larger application state.
Thank you Thomas Visser and Stephen Celis for pointing out some mistakes in early versions of this post! Thank you Janne Siera for adding F# examples to the post!
Bonus: Stick it in a Writer Monad
Reducers show up in two separate places in the comonadic UI framework implementation that I used in barbq. In comonadic UI components, one of the kinds of reducers is located inside a writer monad. Most of the time a component won't need to react to any actions, and if this is the case, components are unencumbered by boilerplate specifying a "dummy" reducer. The writer monad just takes empty
for us. When we need to react to actions here, we can tell
our component how to react to each one. We're able to get this nice API for free precisely because reducers are monoids.