Monads, Functors, and Functional Concepts Simplified

TL;DR: Functors, Applicatives, and Monads are patterns for working with values wrapped in a context (containers) without unwrapping them manually. You’ll learn:
- What a Functor is and why
map
matters - How Applicatives extend Functors with
ap
( <code><*></code> orliftA2
) - What Monads add with
flatMap
/chain
( <code>>=></code>) - JavaScript examples:
Array
,Promise
, and customMaybe
- Pitfalls, best practices, and when to use each abstraction
What Is a Functor?
A Functor is any data structure that implements a map
method, allowing you to apply a function over wrapped values without extracting them. In JavaScript, Array.prototype.map
is the canonical example:
// Array as Functor const numbers = [1, 2, 3] const doubled = numbers.map((x) => x * 2) // [2, 4, 6]
Under the hood, map
takes a function f: A → B
and transforms Functor<A>
into Functor<B>
. The key laws for Functors (so your abstractions behave predictably) are:
- Identity:
F.map(x => x)
is equivalent toF
itself. - Composition:
F.map(x => f(g(x)))
is equivalent toF.map(g).map(f)
.
Any container—Array
, Promise
, even a custom Maybe
—becomes a Functor if it defines map
following these rules.
Applicatives: Functors with Contextual Function Application
Applicatives build on Functors by letting you apply functions inside a context (wrapped in a Functor) to values inside another context. In Haskell notation, Applicative
introduces:
pure
: wrap a plain value into the context.<*>
(pronounced “ap”): givenF<(A → B)>
andF<A>
, produceF<B>
.
In JavaScript, you can see this pattern with Promise.all
+ mapping, or by implementing a custom ap
on Maybe
. For clarity, let’s create a minimal Maybe
and show how ap
works:
// Minimal Maybe implementation class Maybe { constructor(value) { this.value = value } static of(x) { return new Maybe(x) } // Functor map map(fn) { return this.value == null ? Maybe.of(null) : Maybe.of(fn(this.value)) } // Applicative ap ap(maybeWithFn) { return this.value == null || maybeWithFn.value == null ? Maybe.of(null) : Maybe.of(maybeWithFn.value(this.value)) } } // Usage const justAdd = Maybe.of((x) => x + 1) const justTwo = Maybe.of(2) const result = justTwo.ap(justAdd) // Maybe.of(3) const nothing = Maybe.of(null).ap(justAdd) // Maybe.of(null)
Here’s how it works:
Maybe.of(x => x + 1)
wraps a function in theMaybe
context.justTwo.ap(justAdd)
applies that inner function to2
without unwrapping explicitly.- If either side is
null
, theNothing
‐like behavior propagates.
Applicatives let you work with multiple contexts. For example, to add two Maybe
numbers:
const add = (a) => (b) => a + b const maybeA = Maybe.of(3) const maybeB = Maybe.of(4) const sum = maybeB.ap(maybeA.map(add)) // Breakdown: // maybeA.map(add) => Maybe.of(a => a + 3) // maybeB.ap(Maybe.of(a => a + 3)) => Maybe.of(4 + 3) == Maybe.of(7)
Note: ap
is sometimes called liftA2
if you want to lift a normal binary function to two contexts:
const liftA2 = (fn, fa, fb) => fb.ap(fa.map(fn)) const sum2 = liftA2((x, y) => x + y, maybeA, maybeB) // Maybe.of(7)
Enter Monads: Chaining Contextual Computations
Monads extend Applicatives with a flatMap
(or chain
, often represented by >>=
) method, which flattens nested contexts. In practice, flatMap
combines:
- Applying a function that returns a wrapped value:
fn: A → M<B>
- Automatically unwrapping one layer so you end up with
M<B>
instead ofM<M<B>>
.
Why flatMap
Matters
Consider Promise
: each .then()
takes a callback that either returns a value or another Promise
. Behind the scenes, Promise
implements a Monad:
// Promise as Monad Promise.resolve(2) .then((x) => Promise.resolve(x + 3)) // returns Promise<5> .then((result) => console.log(result)) // 5
Without Monad behavior, you’d end up with nested Promise<Promise<…>>
. Thanks to flatMap
-like chaining, the intermediate promise is flattened.
Custom Maybe as Monad
Building on our Maybe
, let’s add flatMap
:
class Maybe { constructor(value) { this.value = value } static of(x) { return new Maybe(x) } map(fn) { return this.value == null ? Maybe.of(null) : Maybe.of(fn(this.value)) } flatMap(fn) { return this.value == null ? Maybe.of(null) : fn(this.value) } } // Example: const safeDivide = (x) => (y) => y === 0 ? Maybe.of(null) : Maybe.of(x / y) Maybe.of(10) .flatMap(safeDivide(2)) // Maybe.of(5) .flatMap(safeDivide(0)) // Maybe.of(null)
Here:
safeDivide(2)
yieldsy => (2 / y or null)
.- First
flatMap
unwrapsMaybe.of(10)
, appliessafeDivide(2)
→Maybe.of(5)
. - Next,
flatMap(safeDivide(0))
unwrapsMaybe.of(5)
, but dividing by 0 yieldsMaybe.of(null)
→ final result isNothing
.
Monad Laws
To keep things predictable, Monads must follow three laws:
- Left Identity:
M.of(x).flatMap(fn)
is equivalent tofn(x)
. - Right Identity:
m.flatMap(M.of)
is equivalent tom
. - Associativity:
m.flatMap(fn).flatMap(g)
is equivalent tom.flatMap(x => fn(x).flatMap(g))
.
When these laws hold, you can safely refactor and compose your monadic code.
JavaScript Examples of Functor/Applicative/Monad
1. Array as Functor (and to some extent, Monad)
- Functor:
array.map(fn)
appliesfn
over each element. - Monad (list monad): Flat-mapping lists—
flatMap(fn)
flattens arrays of arrays.
const nums = [1, 2, 3] const pairs = nums.flatMap((x) => [x, x * 2]) // [1,2, 2,4, 3,6]
2. Promise as Applicative and Monad
- Functor:
.then(result => fn(result))
is likemap
. - Applicative: You can wrap a function in a
Promise
and doPromise.all
. - Monad:
.then
flattens nestedPromise<Promise<…>>
.
const fetchUser = (id) => fetch(`/users/${id}`).then((res) => res.json()) const fetchOrders = (user) => fetch(`/orders/${user.id}`).then((res) => res.json()) // Chained computations via flatMap (Promise.then): fetchUser(42) .then(fetchOrders) .then((orders) => console.log(orders)) .catch((err) => console.error(err))
3. Custom Either for Error Handling
Either
is like Maybe
, but with an error (Left) or success (Right) branch:
class Either { constructor(tag, value) { this.tag = tag this.value = value } static Right(x) { return new Either('Right', x) } static Left(err) { return new Either('Left', err) } map(fn) { return this.tag === 'Right' ? Either.Right(fn(this.value)) : this } flatMap(fn) { return this.tag === 'Right' ? fn(this.value) : this } } // Usage: const parseJSON = (str) => { try { return Either.Right(JSON.parse(str)) } catch (e) { return Either.Left('Invalid JSON') } } parseJSON('{"valid": true}') .flatMap((obj) => Either.Right(obj.valid)) .map((valid) => console.log('Is valid JSON:', valid)) // → Is valid JSON: true parseJSON('invalid') .flatMap((obj) => Either.Right(obj.valid)) .map((valid) => console.log(valid)) // No-op since it’s a Left
Either
conforms to Functor and Monad laws. It helps to avoid nested try/catch
and manual error checks.
When and Why to Use Each Abstraction
- Functor: Use when you have a container-like structure (e.g., arrays, optional values) and you want to transform the inner value without unwrapping.
- Applicative: Use when you have functions inside a context and you need to apply them to values inside another context. Common with validation pipelines or combining multiple
Maybe
/Either
. - Monad: Use when each step can fail or produce a new context. Chaining
Promise
calls, safe division withMaybe
, or error pipelines withEither
.
General Guidelines:
- If you only need to apply a single-argument function over wrapped values,
map
(Functor) is enough. - If you need to apply a function of N arguments where each argument is in a context, reach for Applicative (
ap
,liftA2
, orPromise.all
). - If each step returns a new wrapped value (e.g.,
Promise
,Maybe
, orEither
), and you need to chain them, use Monad (flatMap
/chain
).
Pitfalls & Best Practices
- Breaking Laws: If your
map
,ap
, orflatMap
don’t follow the identity/composition/associativity laws, you’ll introduce subtle bugs when composing. - Overusing Abstractions: Keep it simple—if plain callbacks are clearer, don’t force Monads. Sometimes a straightforward
if/else
is more readable than aMaybe
chain. - Nested Contexts: Be mindful of
F<F<A>>
. If you find yourself callingmap
twice, see if you actually needflatMap
instead. - Callback Hell vs. Monad Hell: Monads tame deeply nested callbacks, but deeply nested monadic chains can still be hard to read. Break logic into small, named functions.
- Performance Considerations: Chaining many small functions can incur tiny overhead. In performance-critical loops, collapsing steps or bypassing abstractions may be warranted.
Conclusion
Functors, Applicatives, and Monads provide a principled way to work with values in context—whether it’s Array
, Promise
, Maybe
, or Either
. By respecting the fundamental laws, you unlock composability and safer pipelines. Start by recognizing existing functors (like Array.map
) in your codebase, then experiment with Applicative patterns (ap
, liftA2
) when combining multiple contexts. Only introduce Monads when you need to chain context-producing functions safely. Over time, these abstractions will feel like natural extensions of your JavaScript toolkit, making code more declarative, predictable, and reusable.
Next Up: Try writing a small Maybe
or Either
utility in your codebase and refactor a sequence of null-checks or try/catch
blocks into monadic chains.