Monadism: Practical FP in TypeScript
Monads are a programming pattern reinforced by category theory that provides for computations which support additional operations, chaining each to the end of the last and transforming the value being managed. They provide a practical way to model sequences of operations, allowing the user to pass the computation to other functions to add additional steps as needed.
A Practical Platform
For years now I've been hunting for a practical, usable, and sound functional programming stack for building applications on the web. In pursuit of this, I've explored a number of different transpile-to-JavaScript stacks and alternative ways to build front ends. I embraced PureScript and Elm and then moved forward with ReasonML for a while, but I never found something that truly struck the balance between usability and soundness that I was looking for.
As a stopgap in my daily work, I turned to TypeScript for a relatively easy-to-use way to keep track of my types and turn more of my thinking into logical assertions that can be validated as I work. I discovered that, over time, Microsoft has turned TypeScript into a very capable and flexible language. With the right settings and tslint configuration, you can use it in a strict way that protects you from a lot of chaos.
TypeScript uses structural typing (similar to the "duck typing" of popular languages like Python), which means that it pays attention to the properties of an object rather than the identity - meaning that an object type is compatible with any other object type with matching properties. In practice, this is sufficient to validate the expected behavior of things like monads. It also provides an increasingly flexible way to manage generic types, including conditional types based on what the user passes in.
Friendly Functionality
I've always missed the confidence that monadic programming gave me, however - the clear understanding of each step in a process, the ability to add new operations and transform values easily, the emphasis on reuse and composition, the protection against things like null
or undefined
. I've never found anything user-friendly enough to use with a team, however. There are libraries like fp-ts that do an outstanding job of providing a comprehensive and full-featured set of functional tools, but they can be overwhelming to the newcomer. I want something that a team can quickly get up to speed on and begin using in an intuitive way.
At Community Funded, we began writing small utilities to help us use TypeScript in a more predictable way, like a small Maybe
(also known as Option
) class to help us represent optional values without relying on null
or undefined
. As these tools grew, we decided to break them off into a separate library that we could share with the world and use in our open source projects. As a result, Monadism was born!
The goal is to build data structures that help us achieve greater determinism and prevent errors, without being inscrutable and hard to maintain. With a solid type structure, you can allow the static analysis to be your guide as you build, maintain, and optimize your code.
A strong type system can not only prevent errors, but also guide you and provide feedback in your design process.
Mark Seemann - Type Driven Development
Maybe Just Nothing
Let's look at some practical examples using the Maybe
monad. A Maybe
is an optional value that can be represented as "absent" via the Nothing()
constructor, or "present" via the Just()
constructor. It allows us to reduce the amount of conditional or boolean logic in our code. To see how this can help, let's look at a typical operation using undefined
for optional values.
// This is a hypothetical function that calls an API endpoint and gets a Page
const getPage = async (slug: string): Promise<Page | undefined> => {
// ...
}
Imagine we have a function that retrieves a Page
from an API. It returns a Promise
with a yield value that can be undefined
if no Page
is found. This object has some nested properties on it that we want to use if a Page
was found. To use them, we need to fill our code with verbose conditional operators.
const page = await getPage('my-page')
const pageId = page && page.id
const pageType = page && page.type
const pageUrl = page && page.url
const attributes: PageAttributes = page && page.attributes && JSON.parse(page.attributes)
const header = attributes && attributes.header
const headerImage = header && header.image
const bannerImage = attributes && attributes.banner && attributes.banner.image
const titleText = header && header.title && header.title.text
const shareUrl = attributes && attributes.share && attributes.share.url
renderPage({
pageId: pageId ? Number(pageId) : undefined,
pageType,
image: (headerImage && headerImage.large)
|| (bannerImage && bannerImage.large)
|| defaultImage.large,
title: titleText || defaultTitle,
url: shareUrl || pageUrl,
})
You might think, "This is 2019! We can use destructuring!" Sadly, TypeScript has other opinions.
const {id, type, url} = page
// Error:
// Property 'id' does not exist on type '{ id: number; type: string; url: string; } | undefined'.
Okay, so how can Maybe
make this better?
const getPage = async (slug: string): Promise<Maybe<Page>> => {
// ...
}
const page = await getPage('my-page')
const attributes = page.prop('attributes').map<PageAttributes>(JSON.parse)
const header = attributes.prop('header')
const banner = attributes.prop('banner')
renderPage({
pageId: page.prop('id').map(Number),
pageType: page.prop('type'),
image: header.prop('image').prop('large')
.alt(banner.prop('image').prop('large'))
.getOr(defaultImage.large),
title: header.prop('title').prop('text').getOr(defaultTitle),
url: attributes.prop('share').prop('url').alt(page.prop('url')),
})
Returning a Maybe<Page>
allows you to produce a much more declarative operation. Rather than using conditional logic and "falsy" values to imperatively control the flow, you describe the results you want and let Maybe
handle the flow for you. This is especially useful for extending and transforming the operation if you re-use it elsewhere, or transforming your wrapper type from Maybe
into other compatible data structures to use it in different ways.
Code that inverts control to the caller is generally easier to understand. [...] If you don’t have any trouble reasoning about code that overflows with booleans and conditionals, then more power to you! For the rest of us, however, conditionals complicate reasoning.
John A. DeGoes - Destroy All Ifs
Channeling Signals
State management is a problem as old as (software engineering) history. There have been a variety of ways devised to keep track of the current status of an application, and they all have pros and cons. One strategy that has enchanted functional programming enthusiasts is functional reactive programming.
FRP is a general paradigm for describing dynamic (time-varying) information. [...] FRP expressions describe entire evolutions of values over time, representing these evolutions directly as first-class values.
Conal Elliot - Essence of FRP
In general, however, these libraries are inspired by the representation of events as a stream over a period of time. Though they may not mathematically qualify as functional reactive programming, they can provide a helpful pattern for modeling and understanding events over time.
In Monadism, we've ported the novel purescript-signal library, inspired by Elm's Signals, to TypeScript. The foldp method allows us to create a "past-dependent" Signal. Each update from the incoming signals will be used to step the state forward. The outgoing signal represents the current state.
The Mario example does a great job of demonstrating Signal's usefulness. The main logic loop is expressed like this:
const jump = (jmp: boolean) => (c: Character): Character => {
if (jmp && !isAirborne(c)) {
return {...c, dy: minJumpSpeed + (jumpCoefficient * Math.abs(c.dx))}
}
if (!jmp && isAirborne(c) && c.dy > 0.0) {
return {...c, dy: c.dy - gravity}
}
return c
}
export const marioLogic = (inputs: Inputs) => compose(
velocity,
applyGravity,
walk(inputs.left, inputs.right),
jump(inputs.jump)
)
You can see the full module in the Monadism repository, but this is how the jump function is implemented. The jump input is a boolean
value representing whether the space bar is pressed at a given moment in time or not.
If the space bar is being pressed and Mario is not airborne yet, then the dy
value representing the y
velocity is set to the appropriate value. If the space bar is not being pressed and Mario is airborne, then he begins to feel the effects of gravity and his y
velocity is decreased.
The marioLogic
function takes the current inputs and returns a function that takes a previous state and returns a next state. This means that the state of the future depends on the state of the past - a foldp
.
const getInputs = (left: boolean) => (right: boolean) => (jump: boolean) => ({left, right, jump})
export const main = () => {
onDOMContentLoaded(() => {
const frames = animationFrame()
const leftInputs = keyPressed(KeyCodes.left)
const rightInputs = keyPressed(KeyCodes.right)
const jumpInputs = keyPressed(KeyCodes.jump)
const inputs = jumpInputs.apply(rightInputs.apply(leftInputs.map(getInputs)))
const game = frames.sampleOn(inputs).foldp(gameLogic, initialState).map(render)
Signal.run(game)
})
}
The main run loop waits until the DOM is loaded, and then creates a Signal based on animation frames. This yields a stream of events for each animation tick.
For the inputs, we create a Signal that yields a stream of values based on whether the given key codes are pressed. Then, we apply them in order to the getInputs
function to populate the plain object representing all of the inputs at once.
For each tick of the animationFrame()
, the inputs are sampled and then piped into the marioLogic
function, which processes updates to the logic ever since an initialState
is established (not pictured in the snippet).
Just Getting Started
The Monadism library is just getting started, but the plan is to keep it lean and practical. Tools that end up not getting a lot of real-world use will be moved into a contrib
package, allowing the library to grow while keeping the core focused on the most widely useful utilities. Documentation and tutorials will grow over time, and hopefully Monadism will become an easy-to-use way to introduce sound functional programming to your production application!
Let me know if you have suggestions or feedback, and find me on Twitter if you want to talk more!