In functional programming, monads are one of the most pattern for structuring code. But if you've just started exploring the world of functors and applicatives, understanding how monads fit into the picture might feel daunting.
In this post, we'll walk through how to implement a monad in JavaScript, using a simple example. We'll also explain how monads differ from functors and applicatives, and how applicatives use the ap
method to apply wrapped functions to wrapped values.
By the end, you'll have a functional, composable monad that can elegantly handle chained operations and errors.
What Are Functors, Applicatives, and Monads?
Before diving into the monad implementation, let's quickly recap these three key concepts in functional programming:
Functor: A functor is a structure that can be mapped over. In JavaScript, arrays are functors because you can apply a function to each element using
map()
. Functors follow the rule:F(a) => F(b)
.Applicative: Applicatives take the concept of functors further. They allow you to apply wrapped functions to wrapped values. Applicatives have an
ap
method that lets you do this:F(f) => F(a) => F(b)
.Monad: Monads extend functors and applicatives with the ability to flatMap (also known as
bind
). This allows you to chain operations that return monads themselves, without ending up with nested structures likeSome(Some(value))
. Monads follow the rule:F(a) => (a => F(b)) => F(b)
.
Implementing the Functor: Maybe
Let's start with a simple functor that we’ll later extend into an applicative and monad. This functor, called Maybe
, can have two possible states: Some (representing a value) or None (representing the absence of a value).
const Maybe = () => ({
map: (f) => new Error("Must implement the method map!"),
pattern: (pattern) => new Error("Must implement the method pattern!"),
ap: (other) => new Error("Must implement the method ap!"),
flatMap: (f) => new Error("Must implement the method flatMap!")
});
This Maybe
functor needs to be fleshed out with concrete implementations for Some and None.
Adding the Some
and None
Functors
We'll define Some
as a functor that wraps a value and applies functions to it, and None
as an empty functor that ignores any function.
const Some = (value) => {
const state = { value };
const map = (f) => Some(f(state.value));
const patternMatching = (pattern) => pattern.some(state.value);
return { ...state, map, pattern: patternMatching };
};
const None = () => ({
map: (f) => None(),
pattern: (pattern) => pattern.none()
});
Some
has amap
function that applies the provided functionf
to the value insideSome
.None
ignores anymap
calls since there's no value to work with.
Adding Applicatives: Implementing ap
Applicatives allow us to apply functions wrapped in the functor to values also wrapped in the functor. This is where the ap
method comes in.
- In
Some
,ap
takes another functor (which contains a function) and applies it to the wrapped value. - In
None
,ap
always returnsNone
because there’s no value to apply the function to.
const Some = (value) => {
const state = { value };
const map = (f) => Some(f(state.value));
const patternMatching = (pattern) => pattern.some(state.value);
// Applicative 'ap' method
const ap = (other) =>
other.pattern({
some: (f) => Some(f(state.value)),
none: () => None()
});
return { ...state, map, pattern: patternMatching, ap };
};
const None = () => ({
map: (f) => None(),
pattern: (pattern) => pattern.none(),
ap: (other) => None() // Always return None
});
With ap
, we can apply a function wrapped in a Some
to a value wrapped in another Some
.
Moving from Applicative to Monad: Adding flatMap
The key feature of a monad is the flatMap
(or bind
) method, which allows you to chain operations that return monads themselves. We'll now implement flatMap
for both Some
and None
.
flatMap
inSome
: Applies a function that returns another monad.flatMap
inNone
: Always returnsNone
.
const Some = (value) => {
const state = { value };
const map = (f) => Some(f(state.value));
const patternMatching = (pattern) => pattern.some(state.value);
const ap = (other) =>
other.pattern({
some: (f) => Some(f(state.value)),
none: () => None()
});
// Monad 'flatMap' method
const flatMap = (f) => f(state.value); // Apply function that returns a monad
return { ...state, map, pattern: patternMatching, ap, flatMap };
};
const None = () => ({
map: (f) => None(),
pattern: (pattern) => pattern.none(),
ap: (other) => None(),
// Monad 'flatMap' method always returns None
flatMap: (f) => None()
});
Test Example: Using map
, ap
, and flatMap
Let’s test this monad by applying transformations using map
, applying functions with ap
, and chaining operations using flatMap
.
const some5 = Some(5);
const someFn = Some((x) => x * 2);
const none = None();
// Using 'map' to transform the value
console.log(some5.map((x) => x + 1).map((x) => x * 2)); // { value: 12 }
// Using 'ap' to apply a function wrapped in a functor
console.log(some5.ap(someFn)); // { value: 10 }
console.log(none.ap(someFn)); // None
console.log(someFn.ap(some5)); // { value: 10 }
console.log(someFn.ap(none)); // None
// Using 'flatMap' to chain monadic operations
console.log(some5.flatMap((x) => Some(x + 10))); // { value: 15 }
console.log(some5.flatMap((x) => Some(x + 10)).flatMap((x) => Some(x * 2))); // { value: 30 }
console.log(none.flatMap((x) => Some(x + 10))); // None
Real-World Example: Safe Data Fetching
Monads can be incredibly useful when dealing with operations that may fail. For example, when fetching data from an API, you can use Maybe
to handle success or failure without writing imperative if/else blocks.
const fetchData = (url) => {
const data = fetch(url).then((res) => res.ok ? Some(res.json()) : None());
return data;
};
fetchData('https://api.example.com/data')
.flatMap((data) => Some(data.user))
.flatMap((user) => Some(user.name))
.pattern({
some: (name) => console.log(`User name: ${name}`),
none: () => console.log('User not found.')
});
Here, flatMap
allows us to keep fetching parts of the data while handling potential failures, like missing or malformed data.
Conclusion
Monads in JavaScript provide an elegant way to chain operations while handling errors or missing values gracefully. By implementing both the ap
(for applicative) and flatMap
(for monad) methods, you unlock the full power of these functional tools, enabling complex workflows to remain predictable and functional.
While this post focuses on Maybe
, the concepts you’ve learned can be applied to other monads like Promise
, Either
, or even your custom monads.
Happy coding!