Lesson 20 / 25
Monads Explained Practically
flatMap and chaining.
Chaining steps that return wrapped values
Problem: you have steps that each return a wrapped value, such as a function returning Option<User> and another taking a User and returning Option<Address>. Using map would give Option<Option<Address>>. A monad adds an operation usually called flatMap, chain, bind or >>= that runs the next step and flattens the result, so a sequence of fallible steps reads as a straight line and stops at the first None or Err. You already use the pattern: Array.prototype.flatMap, Promise.then returning a promise (and async/await), and optional chaining a?.b?.c for nullable values. Monads also need a way to wrap a plain value (of/return/Promise.resolve) and obey laws that make chaining predictable.
flatMap for Option and Result-like chains
Each step can fail; the chain short-circuits.
type Option<T> = { tag: 'some'; value: T } | { tag: 'none' };
const some = <T>(value: T): Option<T> => ({ tag: 'some', value });
const none: Option<never> = { tag: 'none' };
const flatMap = <A, B>(o: Option<A>, f: (a: A) => Option<B>): Option<B> =>
o.tag === 'some' ? f(o.value) : none;
type User = { id: string; addressId?: string };
type Address = { city: string };
declare function findUser(id: string): Option<User>;
declare function findAddress(id: string): Option<Address>;
const cityOf = (userId: string): Option<string> =>
flatMap(findUser(userId), u =>
flatMap(u.addressId ? some(u.addressId) : none, aid =>
flatMap(findAddress(aid), a => some(a.city))));
// The same shape you already know from promises:
// fetchUser(id).then(u => fetchAddress(u.addressId)).then(a => a.city)
// And from arrays:
[[1, 2], [3]].flatMap(xs => xs); // [1, 2, 3]do-notation and async/await
Haskell's do blocks and JavaScript's async/await both exist to hide nested chaining: they let monadic code read like ordinary sequential statements.
Quick check: What does flatMap add compared with map?
- It sorts the values
- It flattens the result when the function itself returns a wrapped value
- It runs the function twice
- It removes the need for a container
Answer
It flattens the result when the function itself returns a wrapped value — map would nest: Option<Option<T>>.