Functors and Monads
Functor
Section titled “Functor”A functor is a container that can be mapped over, preserving structure while transforming values.
Functor Laws
Section titled “Functor Laws”- Identity:
functor.map(x => x)equalsfunctor - Composition:
functor.map(f).map(g)equalsfunctor.map(x => g(f(x)))
Array as Functor
Section titled “Array as Functor”// Arrays are functorsconst numbers = [1, 2, 3];
// Map preserves structure (still an array)const doubled = numbers.map(x => x * 2);// [2, 4, 6]
// Identity lawconst identity = numbers.map(x => x);// [1, 2, 3] - same as original
// Composition lawconst addOne = (x: number) => x + 1;const double = (x: number) => x * 2;
const composed1 = numbers.map(addOne).map(double);const composed2 = numbers.map(x => double(addOne(x)));// Both produce [4, 6, 8]Maybe/Option Functor
Section titled “Maybe/Option Functor”Handle nullable values safely without null checks.
Maybe Implementation
Section titled “Maybe Implementation”type Maybe<T> = Some<T> | None;
class Some<T> { constructor(private value: T) {}
map<U>(fn: (value: T) => U): Maybe<U> { return new Some(fn(this.value)); }
flatMap<U>(fn: (value: T) => Maybe<U>): Maybe<U> { return fn(this.value); }
getOrElse(defaultValue: T): T { return this.value; }
isNone(): boolean { return false; }}
class None implements Maybe<never> { map<U>(_fn: (value: never) => U): Maybe<U> { return new None(); }
flatMap<U>(_fn: (value: never) => Maybe<U>): Maybe<U> { return new None(); }
getOrElse<T>(defaultValue: T): T { return defaultValue; }
isNone(): boolean { return true; }}
// Helper functionfunction maybe<T>(value: T | null | undefined): Maybe<T> { return value != null ? new Some(value) : new None();}Maybe Usage
Section titled “Maybe Usage”// Without Maybe - lots of null checksfunction getUserEmail(userId: number): string | null { const user = findUser(userId); if (!user) return null;
const profile = user.profile; if (!profile) return null;
const email = profile.email; if (!email) return null;
return email.toLowerCase();}
// With Maybe - clean compositionfunction getUserEmailSafe(userId: number): Maybe<string> { return maybe(findUser(userId)) .flatMap(user => maybe(user.profile)) .flatMap(profile => maybe(profile.email)) .map(email => email.toLowerCase());}
// Usageconst email = getUserEmailSafe(123).getOrElse('no-email@example.com');Chaining Operations
Section titled “Chaining Operations”// Chain multiple operations safelyconst result = maybe(5) .map(x => x * 2) // Some(10) .map(x => x + 1) // Some(11) .map(x => x.toString()) // Some('11') .getOrElse('default'); // '11'
// None propagates through chainconst nullResult = maybe(null) .map(x => x * 2) .map(x => x + 1) .getOrElse(0); // 0Either/Result Monad
Section titled “Either/Result Monad”Handle errors without exceptions, preserving error information.
Either Implementation
Section titled “Either Implementation”type Either<E, T> = Left<E> | Right<T>;
class Left<E> { constructor(private error: E) {}
map<U>(_fn: (value: never) => U): Either<E, U> { return new Left(this.error); }
flatMap<U>(_fn: (value: never) => Either<E, U>): Either<E, U> { return new Left(this.error); }
getOrElse<T>(defaultValue: T): T { return defaultValue; }
isLeft(): boolean { return true; }
fold<U>(onLeft: (error: E) => U, _onRight: (value: never) => U): U { return onLeft(this.error); }}
class Right<T> { constructor(private value: T) {}
map<U>(fn: (value: T) => U): Either<never, U> { return new Right(fn(this.value)); }
flatMap<E, U>(fn: (value: T) => Either<E, U>): Either<E, U> { return fn(this.value); }
getOrElse(_defaultValue: T): T { return this.value; }
isLeft(): boolean { return false; }
fold<U>(_onLeft: (error: never) => U, onRight: (value: T) => U): U { return onRight(this.value); }}Either Usage
Section titled “Either Usage”function divide(a: number, b: number): Either<string, number> { return b === 0 ? new Left('Division by zero') : new Right(a / b);}
function safeSqrt(n: number): Either<string, number> { return n < 0 ? new Left('Cannot take square root of negative number') : new Right(Math.sqrt(n));}
// Chain operations that can failconst result = divide(10, 2) .flatMap(x => safeSqrt(x)) .map(x => x * 2);
result.fold( error => console.error('Error:', error), value => console.log('Result:', value));// Result: ~4.47
// Error caseconst errorResult = divide(10, 0) .flatMap(x => safeSqrt(x)) .map(x => x * 2);
errorResult.fold( error => console.error('Error:', error), value => console.log('Result:', value));// Error: Division by zeroValidation Example
Section titled “Validation Example”interface User { email: string; age: number; name: string;}
function validateEmail(email: string): Either<string, string> { return email.includes('@') ? new Right(email) : new Left('Invalid email format');}
function validateAge(age: number): Either<string, number> { return age >= 18 && age <= 120 ? new Right(age) : new Left('Age must be between 18 and 120');}
function validateName(name: string): Either<string, string> { return name.length >= 2 ? new Right(name) : new Left('Name must be at least 2 characters');}
function validateUser(user: User): Either<string, User> { return validateEmail(user.email) .flatMap(() => validateAge(user.age)) .flatMap(() => validateName(user.name)) .map(() => user);}
// Usageconst validUser = validateUser({ email: 'user@example.com', age: 25, name: 'John'});
validUser.fold( error => console.error('Validation failed:', error), user => console.log('Valid user:', user));Task/IO Monad
Section titled “Task/IO Monad”Lazy evaluation of side effects.
Task Implementation
Section titled “Task Implementation”class Task<T> { constructor(private computation: () => Promise<T>) {}
map<U>(fn: (value: T) => U): Task<U> { return new Task(async () => { const value = await this.computation(); return fn(value); }); }
flatMap<U>(fn: (value: T) => Task<U>): Task<U> { return new Task(async () => { const value = await this.computation(); const task = fn(value); return task.run(); }); }
run(): Promise<T> { return this.computation(); }}
// Helperfunction task<T>(computation: () => Promise<T>): Task<T> { return new Task(computation);}Task Usage
Section titled “Task Usage”// Define tasks (not executed yet)const fetchUser = (id: number) => task(() => fetch(`/api/users/${id}`).then(r => r.json()));
const fetchPosts = (userId: number) => task(() => fetch(`/api/users/${userId}/posts`).then(r => r.json()));
// Compose tasksconst getUserWithPosts = (id: number) => fetchUser(id).flatMap(user => fetchPosts(user.id).map(posts => ({ ...user, posts })) );
// Execute only when neededconst result = await getUserWithPosts(1).run();Monad Laws
Section titled “Monad Laws”All monads must satisfy three laws:
1. Left Identity
Section titled “1. Left Identity”// Wrapping a value and flatMapping should be the same as just applying the functionconst value = 5;const f = (x: number) => new Right(x * 2);
// These should be equivalent:new Right(value).flatMap(f); // Right(10)f(value); // Right(10)2. Right Identity
Section titled “2. Right Identity”// flatMapping with the constructor should return the same monadconst monad = new Right(5);
monad.flatMap(x => new Right(x)); // Right(5)monad; // Right(5)3. Associativity
Section titled “3. Associativity”// The order of flatMap operations shouldn't matterconst monad = new Right(5);const f = (x: number) => new Right(x + 1);const g = (x: number) => new Right(x * 2);
// These should be equivalent:monad.flatMap(f).flatMap(g);monad.flatMap(x => f(x).flatMap(g));Practical Patterns
Section titled “Practical Patterns”Combining Multiple Eithers
Section titled “Combining Multiple Eithers”function sequence<E, T>(eithers: Either<E, T>[]): Either<E, T[]> { const results: T[] = [];
for (const either of eithers) { if (either.isLeft()) { return either as Either<E, T[]>; } results.push(either.fold(() => null as never, x => x)); }
return new Right(results);}
// Usageconst results = [ divide(10, 2), divide(20, 4), divide(30, 5)];
sequence(results).fold( error => console.error('Error:', error), values => console.log('All results:', values));Traversing with Effects
Section titled “Traversing with Effects”function traverse<T, U>( array: T[], fn: (value: T) => Maybe<U>): Maybe<U[]> { const results: U[] = [];
for (const item of array) { const result = fn(item); if (result.isNone()) { return new None(); } results.push(result.getOrElse(null as never)); }
return new Some(results);}
// Usageconst numbers = ['1', '2', '3'];
const parsed = traverse(numbers, str => { const num = parseInt(str, 10); return isNaN(num) ? new None() : new Some(num);});
parsed.getOrElse([]); // [1, 2, 3]Best Practices
Section titled “Best Practices”✅ Use Maybe for nullable values ✅ Use Either for error handling ✅ Chain operations with flatMap ✅ Keep monadic operations pure ✅ Use fold to extract values ✅ Leverage type safety ✅ Compose monadic functions ✅ Handle errors explicitly
Don’ts
Section titled “Don’ts”❌ Use exceptions with Either ❌ Mix null checks with Maybe ❌ Forget to handle error cases ❌ Create deep nesting with flatMap ❌ Ignore monad laws ❌ Use monads for simple values ❌ Over-complicate simple code