Dataslope logoDataslope

Category Theory Intuitions

Functor, Applicative, and Monad — explained as everyday patterns you've already been using.

Category theory has a fearsome reputation. The good news is that every working FP programmer uses three category-theoretic patterns daily — usually without naming them. This chapter gives you the names and, more importantly, the intuitions.

We'll keep all the code in plain TypeScript. No libraries, no Greek letters.

A unifying observation

You already know Array.prototype.map. And you've just met Option's map and Result's map. And Promise's .then is basically map for asynchronous values.

That's not an accident. The same operation keeps appearing for different "container" types. When a pattern repeats across many unrelated types, it's worth giving it a name.

TypeMeansmap transforms…
Array<A>"zero or more A"every element
Option<A>"maybe an A"the value if present
Result<E,A>"A or an error E"the value if ok
Promise<A>"an A arriving later"the eventual value
Task<A>"an A you can run later"the produced value
(R) => A"an A given a context R"the produced value

All of these are functors. A functor is just a type F<A> together with a lawful map operation.

Functor

Code Block
TypeScript 5.7

Functor laws (intuitions)

A functor's map must obey two laws — both perfectly sensible:

  1. Identity: map(fa, x => x) === fa — mapping with the identity function changes nothing.
  2. Composition: map(map(fa, f), g) === map(fa, x => g(f(x))) — mapping with f then g is the same as mapping with g ∘ f.

These laws guarantee map is "structure-preserving": it doesn't secretly rearrange the container, drop elements, duplicate things, or fail in surprising ways. If you've ever felt that array.map((x) => x) should equal array — that's the functor identity law.

The two paths must give the same result. Always.

Applicative

Functors lift a one-argument function into the container. What about two-argument functions? That's where Applicative comes in. It adds two extras:

  • of(a) — put a plain value into the container ("wrap")
  • ap(ff, fa) — given a wrapped function and a wrapped value, apply one to the other.

With those, you can lift any n-argument function.

Code Block
TypeScript 5.7

The intuition: applicative is for combining independent computations. "Get config A AND config B in parallel, then combine them" is applicative. Either both succeed, or the whole thing fails (if you use the validation-style applicative, both errors even get collected — see below).

Accumulating validation errors

A famous use of applicative: validate several fields and gather all the errors, not just the first one.

Code Block
TypeScript 5.7

That's the practical superpower of applicative: form validation that reports every problem at once.

Monad

Finally, Monad is the pattern for dependent chained computations: "compute A; then, using A, compute B; then, using B, compute C".

A monad adds flatMap (also called chain/bind/andThen) to a functor:

flatMap<A, B>(fa: F<A>, f: (a: A) => F<B>): F<B>

The difference from map:

  • map takes (a: A) => B — the inner function returns a bare value.
  • flatMap takes (a: A) => F<B> — the inner function itself returns a container.

flatMap flattens the resulting F<F<B>> back to F<B>. That flattening is the whole point: each step contributes its own "context" (failure / asynchrony / nondeterminism), and flatMap merges them into one.

Code Block
TypeScript 5.7

Functor vs. Applicative vs. Monad

A useful mnemonic:

PatternUse when…
FunctorYou have one container; you want to transform the inside.
ApplicativeYou have several independent containers; you want to combine.
MonadYou have a chain where each step depends on the previous.

A real example combining all three:

// Independent: validate name AND age in parallel  -> Applicative
const personR = map2((n, a) => ({ name: n, age: a }), nameR, ageR);

// Then dependent: lookup user by valid id          -> Monad
const userR = flatMap(personR, (p) => lookupByName(p.name));

// Then transform the user                          -> Functor
const greeting = map(userR, (u) => `Hello, ${u.name}!`);

The three patterns compose. Real FP code is a mix of all three, chosen per step depending on whether things are independent or sequential.

Why this matters

Once you see Functor / Applicative / Monad, you start noticing them everywhere: lists, options, results, promises, parsers, streams, optics, state, reducers, even React reducers and Redux thunks. They're not magic — they're just the shape of code that threads a context through.

Libraries like fp-ts, Effect, and zio formalize these. But the patterns belong to the structure of computation, not to any library. You can — and we have — write them in plain TypeScript in a dozen lines.

A small challenge

Challenge
TypeScript 5.7
Use applicative to combine two Options

In combine.ts, implement map2(f, oa, ob) for Option: it returns some(f(a, b)) if both are some, otherwise none.

Then in main.ts, use it to compute pair(some(2), some(3)), pair(none(), some(3)), and pair(some(2), none()).

Expected output

{ kind: 'some', value: [ 2, 3 ] }
{ kind: 'none' }
{ kind: 'none' }

QuestionSelect one

When should you reach for flatMap (monad) instead of map (functor)?

Whenever the operation might throw an error

When the function you want to apply returns another value of the same container type — for example (a: A) => Option<B> — and you want a flat Option<B> instead of Option<Option<B>>

When you want to combine two independent containers

When you want to discard the wrapper completely

On this page