Lesson 8 of 8
Functors & Monads
Understanding functors, monads, Maybe, Either, and handling side effects functionally
What is a Functor?
A Functor is any data type that implements map and follows certain rules.
It's a container that can be mapped over. You already use functors all the time - arrays are functors!
// Arrays are functors - they implement map
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]
// Functor Laws:
// 1. Identity: x.map(a => a) === x
[1, 2, 3].map(x => x); // [1, 2, 3] ✓
// 2. Composition: x.map(f).map(g) === x.map(x => g(f(x)))
const f = x => x + 1;
const g = x => x * 2;
[1, 2, 3].map(f).map(g); // [4, 6, 8]
[1, 2, 3].map(x => g(f(x))); // [4, 6, 8] ✓
// Create a simple functor (Box)
const Box = value => ({
map: fn => Box(fn(value)),
fold: fn => fn(value), // Extract value
value,
});
const result = Box(5)
.map(x => x + 1)
.map(x => x * 2)
.fold(x => x); // 12
The Maybe Monad
Maybe (also called Optional) handles null/undefined safely. It's either
Just(value) (has a value) or Nothing (no value).
// Problem: Null checks are tedious and error-prone
const user = { name: 'Alice', address: { city: 'NYC' } };
const city = user && user.address && user.address.city; // 😫
// Maybe to the rescue!
const Maybe = {
Just: value => ({
map: fn => Maybe.Just(fn(value)),
flatMap: fn => fn(value),
getOrElse: () => value,
isNothing: false,
}),
Nothing: () => ({
map: () => Maybe.Nothing(),
flatMap: () => Maybe.Nothing(),
getOrElse: defaultValue => defaultValue,
isNothing: true,
}),
fromNullable: value =>
value == null ? Maybe.Nothing() : Maybe.Just(value),
};
// Safe property access
const prop = key => obj => Maybe.fromNullable(obj[key]);
const getCity = user =>
Maybe.fromNullable(user)
.flatMap(prop('address'))
.flatMap(prop('city'));
getCity({ address: { city: 'NYC' }}).getOrElse('Unknown'); // 'NYC'
getCity({ address: {} }).getOrElse('Unknown'); // 'Unknown'
getCity(null).getOrElse('Unknown'); // 'Unknown'
// Chain operations safely
Maybe.fromNullable(user)
.map(u => u.name)
.map(name => name.toUpperCase())
.getOrElse('ANONYMOUS'); // 'ALICE'
The Either Monad
Either represents a value that can be one of two types: Left
(typically error/failure) or Right (success). It's great for error handling.
// Either implementation
const Either = {
Right: value => ({
map: fn => Either.Right(fn(value)),
flatMap: fn => fn(value),
fold: (leftFn, rightFn) => rightFn(value),
isRight: true,
isLeft: false,
}),
Left: value => ({
map: () => Either.Left(value), // Don't map over Left
flatMap: () => Either.Left(value),
fold: (leftFn, rightFn) => leftFn(value),
isRight: false,
isLeft: true,
}),
tryCatch: fn => {
try {
return Either.Right(fn());
} catch (e) {
return Either.Left(e);
}
},
};
// Example: Parse JSON safely
const parseJSON = str =>
Either.tryCatch(() => JSON.parse(str));
parseJSON('{"name": "Alice"}')
.map(obj => obj.name)
.fold(
err => `Error: ${err.message}`,
name => `Hello, ${name}`
); // 'Hello, Alice'
parseJSON('invalid json')
.map(obj => obj.name)
.fold(
err => `Error: ${err.message}`,
name => `Hello, ${name}`
); // 'Error: Unexpected token...'
// Validation example
const validateEmail = email =>
email.includes('@')
? Either.Right(email)
: Either.Left('Invalid email');
const validatePassword = password =>
password.length >= 8
? Either.Right(password)
: Either.Left('Password too short');
validateEmail('user@example.com')
.flatMap(() => validatePassword('abc'))
.fold(
err => ({ success: false, error: err }),
() => ({ success: true })
); // { success: false, error: 'Password too short' }
What is a Monad?
A Monad is a functor that also implements flatMap (also called
chain or bind). It allows you to chain operations that return wrapped values.
// The problem: nested containers
const nested = Maybe.Just(Maybe.Just(5));
// We want Just(5), not Just(Just(5))
// map creates nesting
Maybe.Just(5).map(x => Maybe.Just(x + 1)); // Just(Just(6)) 😫
// flatMap unwraps one level
Maybe.Just(5).flatMap(x => Maybe.Just(x + 1)); // Just(6) ✓
// Monad Laws:
// 1. Left Identity: M.of(a).flatMap(f) === f(a)
// 2. Right Identity: m.flatMap(M.of) === m
// 3. Associativity: m.flatMap(f).flatMap(g) === m.flatMap(x => f(x).flatMap(g))
// Practical use: chaining async-like operations
const getUser = id => Maybe.fromNullable(users[id]);
const getAddress = user => Maybe.fromNullable(user.address);
const getCity = address => Maybe.fromNullable(address.city);
// Without flatMap - nested nightmare
getUser(1).map(user =>
getAddress(user).map(address =>
getCity(address))); // Maybe(Maybe(Maybe(city)))
// With flatMap - clean chain
getUser(1)
.flatMap(getAddress)
.flatMap(getCity)
.getOrElse('Unknown'); // 'NYC' or 'Unknown'
Practical FP Error Handling
// Real-world example: API call with validation
const fetchUserData = async userId => {
const validateId = id =>
id > 0
? Either.Right(id)
: Either.Left('Invalid user ID');
const fetchUser = async id => {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) return Either.Left('User not found');
return Either.Right(await res.json());
} catch (e) {
return Either.Left('Network error');
}
};
const validateAge = user =>
user.age >= 18
? Either.Right(user)
: Either.Left('User must be 18+');
// Chain it all together
const result = validateId(userId);
if (result.isLeft) return result;
const userResult = await fetchUser(userId);
if (userResult.isLeft) return userResult;
return validateAge(userResult.fold(() => null, u => u));
};
// Usage
const result = await fetchUserData(123);
result.fold(
error => console.error(error),
user => console.log('Valid user:', user)
);
Libraries for FP in JavaScript
// fp-ts - Full FP in TypeScript
import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either';
const result = pipe(
O.some({ name: 'Alice' }),
O.map(user => user.name),
O.map(name => name.toUpperCase()),
O.getOrElse(() => 'ANONYMOUS')
);
// Ramda - Practical FP utilities
import * as R from 'ramda';
const processUsers = R.pipe(
R.filter(R.propEq('active', true)),
R.map(R.prop('name')),
R.take(5)
);
// Sanctuary - FP with strict types
import S from 'sanctuary';
S.pipe([
S.map(x => x + 1),
S.filter(x => x > 5),
])(S.Just([1, 2, 3, 4, 5, 6]));
✅ When to Use These Patterns
- • Maybe: When a value might not exist (null/undefined handling)
- • Either: When operations can fail with meaningful errors
- • Functors: When you need to transform values in a container
- • Monads: When chaining operations that return containers