| name | functional-programming |
| description | Master functional programming principles and patterns including purity, immutability, composition, higher-order functions, algebraic data types, functors, monads, and effect management. Use when working with functional paradigms, pure functions, immutable data structures, function composition, type-safe error handling, or implementing functional patterns like map/filter/reduce, currying, partial application, recursion, lazy evaluation, or referential transparency. |
Functional Programming Mastery
Core Principles
The Zen of Functional Programming
Purity is better than side effects
Pure functions create predictable, testable code. Functions depend only on inputs and produce no side effects.
Immutability is better than mutation
Data that cannot change eliminates entire categories of bugs. State cannot be modified; concurrent operations become naturally safe.
Composition is better than monoliths
Small, focused functions combined thoughtfully solve complex problems elegantly.
Declarative is better than imperative
Express what you want, not how to get it.
Referential transparency enables reasoning
When expressions can be replaced by their values, code becomes mathematical.
Functions are first-class citizens
Treat functions as data. Pass them, return them, compose them.
Explicit is better than hidden
Make dependencies visible through function parameters. No global state, no hidden coupling.
Recursion is iteration without mutation
When loops require changing variables, let recursion express repetition through function calls.
Type safety catches errors early
Strong types are your allies. Let the compiler prove code correct before it runs.
Separate effects from logic
Isolate impure operations from pure computation. Draw clear boundaries between the ideal and the real.
Statelessness enables concurrency
Functions without shared state run safely in parallel.
Errors should be values, not exceptions
Make failure explicit in types. Return Maybe, Either, Result.
Although practicality beats purity
The real world requires side effects. Embrace them thoughtfully, manage them explicitly, isolate them carefully.
Fundamental Concepts
Pure Functions
Pure functions have two key properties:
- Deterministic: Same input always produces same output
- No side effects: No mutations, no I/O, no observable changes
const add = (a: number, b: number): number => a + b;
let total = 0;
const addToTotal = (n: number): void => { total += n; };
const logAndAdd = (a: number, b: number): number => {
console.log('adding');
return a + b;
};
Immutability
Never mutate data; always create new values.
const addItem = (arr: number[], item: number): void => {
arr.push(item);
};
const addItem = (arr: readonly number[], item: number): readonly number[] =>
[...arr, item];
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
Function Composition
Build complex operations from simple functions.
const compose = <A, B, C>(f: (b: B) => C, g: (a: A) => B) =>
(a: A): C => f(g(a));
const pipe = <A, B, C>(f: (a: A) => B, g: (b: B) => C) =>
(a: A): C => g(f(a));
const double = (n: number): number => n * 2;
const increment = (n: number): number => n + 1;
const doubleThenIncrement = pipe(double, increment);
doubleThenIncrement(5);
Higher-Order Functions
Functions that take or return functions.
const map = <A, B>(f: (a: A) => B) =>
(arr: readonly A[]): readonly B[] =>
arr.map(f);
const add = (a: number) => (b: number): number => a + b;
const add5 = add(5);
const compose = <A, B, C>(f: (b: B) => C) =>
(g: (a: A) => B) =>
(a: A): C => f(g(a));
Currying and Partial Application
Transform multi-argument functions into chains of single-argument functions.
const add = (a: number, b: number, c: number): number => a + b + c;
const addCurried = (a: number) => (b: number) => (c: number): number =>
a + b + c;
const add5 = addCurried(5);
const add5and3 = add5(3);
add5and3(2);
type Curry<T extends any[], R> =
T extends [infer First, ...infer Rest]
? (arg: First) => Rest extends [] ? R : Curry<Rest, R>
: R;
const curry = <T extends any[], R>(
fn: (...args: T) => R
): Curry<T, R> => {
return ((...args: any[]) =>
args.length >= fn.length
? fn(...args as T)
: curry((fn as any).bind(null, ...args))
) as any;
};
Algebraic Data Types
Sum Types (Union Types)
Represent "one of several" options.
type Option<A> =
| { tag: 'Some'; value: A }
| { tag: 'None' };
type Either<E, A> =
| { tag: 'Left'; value: E }
| { tag: 'Right'; value: A };
type Result<E, A> = Either<E, A>;
type PaymentMethod =
| { type: 'CreditCard'; cardNumber: string }
| { type: 'PayPal'; email: string }
| { type: 'BankTransfer'; accountNumber: string };
Product Types (Records/Tuples)
Combine multiple values.
type Person = {
readonly name: string;
readonly age: number;
};
type Coordinate = readonly [number, number];
type Address = {
readonly street: string;
readonly city: string;
readonly zipCode: string;
};
type Customer = {
readonly person: Person;
readonly address: Address;
};
Functors
Containers that can be mapped over.
interface Functor<F> {
map<A, B>(fa: F, f: (a: A) => B): F;
}
const arrayMap = <A, B>(arr: readonly A[], f: (a: A) => B): readonly B[] =>
arr.map(f);
const optionMap = <A, B>(opt: Option<A>, f: (a: A) => B): Option<B> =>
opt.tag === 'None' ? opt : { tag: 'Some', value: f(opt.value) };
Monads
Functors with additional structure for sequencing operations.
interface Monad<M> extends Functor<M> {
of<A>(a: A): M;
flatMap<A, B>(ma: M, f: (a: A) => M): M;
}
const optionOf = <A>(value: A): Option<A> =>
({ tag: 'Some', value });
const optionFlatMap = <A, B>(
opt: Option<A>,
f: (a: A) => Option<B>
): Option<B> =>
opt.tag === 'None' ? opt : f(opt.value);
const eitherOf = <E, A>(value: A): Either<E, A> =>
({ tag: 'Right', value });
const eitherFlatMap = <E, A, B>(
either: Either<E, A>,
f: (a: A) => Either<E, B>
): Either<E, B> =>
either.tag === 'Left' ? either : f(either.value);
Common Patterns
Pattern Matching
Safe exhaustive checking of sum types.
const match = <T extends { tag: string }, R>(
value: T,
patterns: { [K in T['tag']]: (val: Extract<T, { tag: K }>) => R }
): R => {
return patterns[value.tag](value as any);
};
const describeOption = <A>(opt: Option<A>): string =>
match(opt, {
Some: ({ value }) => `Value: ${value}`,
None: () => 'No value'
});
Recursion Patterns
Replace loops with recursive functions.
const sum = (arr: readonly number[]): number => {
const go = (i: number, acc: number): number =>
i >= arr.length ? acc : go(i + 1, acc + arr[i]);
return go(0, 0);
};
type Tree<A> =
| { tag: 'Leaf'; value: A }
| { tag: 'Branch'; left: Tree<A>; right: Tree<A> };
const mapTree = <A, B>(tree: Tree<A>, f: (a: A) => B): Tree<B> =>
tree.tag === 'Leaf'
? { tag: 'Leaf', value: f(tree.value) }
: {
tag: 'Branch',
left: mapTree(tree.left, f),
right: mapTree(tree.right, f)
};
Trampolining
Optimize recursion to avoid stack overflow.
type Trampoline<A> =
| { tag: 'Done'; value: A }
| { tag: 'Continue'; next: () => Trampoline<A> };
const run = <A>(trampoline: Trampoline<A>): A => {
let current = trampoline;
while (current.tag === 'Continue') {
current = current.next();
}
return current.value;
};
const factorial = (n: number, acc = 1): Trampoline<number> =>
n <= 1
? { tag: 'Done', value: acc }
: { tag: 'Continue', next: () => factorial(n - 1, n * acc) };
run(factorial(10000));
Lazy Evaluation
Defer computation until needed.
type Lazy<A> = () => A;
const lazy = <A>(compute: () => A): Lazy<A> => {
let cached: { computed: boolean; value?: A } = { computed: false };
return () => {
if (!cached.computed) {
cached.value = compute();
cached.computed = true;
}
return cached.value!;
};
};
type Stream<A> = {
head: () => A;
tail: () => Stream<A>;
};
const naturals = (n = 0): Stream<number> => ({
head: () => n,
tail: () => naturals(n + 1)
});
const take = <A>(n: number, stream: Stream<A>): A[] => {
const result: A[] = [];
let current = stream;
for (let i = 0; i < n; i++) {
result.push(current.head());
current = current.tail();
}
return result;
};
Effect Management
IO Monad
Encapsulate side effects.
type IO<A> = () => A;
const ioOf = <A>(value: A): IO<A> => () => value;
const ioMap = <A, B>(io: IO<A>, f: (a: A) => B): IO<B> =>
() => f(io());
const ioFlatMap = <A, B>(io: IO<A>, f: (a: A) => IO<B>): IO<B> =>
() => f(io())();
const log = (message: string): IO<void> =>
() => console.log(message);
const prompt = (question: string): IO<string> =>
() => window.prompt(question) || '';
const program = ioFlatMap(
prompt('What is your name?'),
name => log(`Hello, ${name}!`)
);
program();
Reader Monad
Thread configuration through computations.
type Reader<R, A> = (env: R) => A;
const readerOf = <R, A>(value: A): Reader<R, A> =>
() => value;
const readerMap = <R, A, B>(
reader: Reader<R, A>,
f: (a: A) => B
): Reader<R, B> =>
env => f(reader(env));
const readerFlatMap = <R, A, B>(
reader: Reader<R, A>,
f: (a: A) => Reader<R, B>
): Reader<R, B> =>
env => f(reader(env))(env);
const ask = <R>(): Reader<R, R> => env => env;
type Config = { apiUrl: string; timeout: number };
const getApiUrl: Reader<Config, string> =
config => config.apiUrl;
const fetchUser = (id: number): Reader<Config, Promise<User>> =>
env => fetch(`${env.apiUrl}/users/${id}`).then(r => r.json());
Free Monad
Separate program description from interpretation.
type Free<F, A> =
| { tag: 'Pure'; value: A }
| { tag: 'Impure'; command: F; next: (x: any) => Free<F, A> };
const pure = <F, A>(value: A): Free<F, A> =>
({ tag: 'Pure', value });
const liftF = <F, A>(command: F): Free<F, A> =>
({ tag: 'Impure', command, next: pure });
const freeMap = <F, A, B>(
free: Free<F, A>,
f: (a: A) => B
): Free<F, B> =>
free.tag === 'Pure'
? pure(f(free.value))
: { ...free, next: x => freeMap(free.next(x), f) };
const freeFlatMap = <F, A, B>(
free: Free<F, A>,
f: (a: A) => Free<F, B>
): Free<F, B> =>
free.tag === 'Pure'
? f(free.value)
: { ...free, next: x => freeFlatMap(free.next(x), f) };
Performance Considerations
Memoization
Cache pure function results.
const memoize = <A extends any[], R>(
fn: (...args: A) => R
): ((...args: A) => R) => {
const cache = new Map<string, R>();
return (...args: A): R => {
const key = JSON.stringify(args);
if (!cache.has(key)) {
cache.set(key, fn(...args));
}
return cache.get(key)!;
};
};
const fib = memoize((n: number): number =>
n <= 1 ? n : fib(n - 1) + fib(n - 2)
);
Transducers
Compose data transformations without intermediate collections.
type Reducer<A, B> = (acc: B, value: A) => B;
type Transducer<A, B, C, D> = (reducer: Reducer<C, D>) => Reducer<A, B>;
const mapT = <A, B>(f: (a: A) => B): Transducer<A, any, B, any> =>
reducer => (acc, value) => reducer(acc, f(value));
const filterT = <A>(pred: (a: A) => boolean): Transducer<A, any, A, any> =>
reducer => (acc, value) => pred(value) ? reducer(acc, value) : acc;
const transduce = <A, B, C>(
transducer: Transducer<A, B, A, C>,
reducer: Reducer<A, C>,
initial: C,
collection: readonly A[]
): C => {
const xf = transducer(reducer);
return collection.reduce(xf, initial);
};
Testing Pure Functions
type Gen<A> = () => A;
const genNumber: Gen<number> = () => Math.random() * 1000;
const genString: Gen<string> = () => Math.random().toString(36);
const property = <A>(
name: string,
gen: Gen<A>,
pred: (a: A) => boolean,
iterations = 100
): void => {
for (let i = 0; i < iterations; i++) {
const value = gen();
if (!pred(value)) {
throw new Error(`Property "${name}" failed for: ${value}`);
}
}
};
property(
'add is commutative',
() => ({ a: genNumber(), b: genNumber() }),
({ a, b }) => add(a, b) === add(b, a)
);
property(
'map identity',
() => [1, 2, 3, 4, 5],
arr => arrayMap(arr, x => x) === arr
);
Best Practices
- Start pure, end impure: Keep business logic pure; push effects to boundaries
- Use readonly liberally: Make immutability explicit in types
- Compose small functions: Build complex operations from simple, testable pieces
- Make illegal states unrepresentable: Use types to prevent invalid data
- Prefer expressions over statements: Every operation should return a value
- Avoid null/undefined: Use Option/Maybe types instead
- Make errors explicit: Use Either/Result instead of throwing exceptions
- Use const over let: Prevent reassignment at language level
- Leverage type inference: Let compiler deduce types where possible
- Document with types: Types are better documentation than comments
Common Pitfalls
- Forgetting to return in arrow functions:
x => { x + 1 } returns undefined
- Mutating in array methods:
arr.sort() mutates; use [...arr].sort()
- Shallow copying objects:
{...obj} only copies top level; nested objects shared
- Overusing composition: Sometimes direct implementation is clearer
- Ignoring performance: Immutability and composition have costs
- Fighting the language: TypeScript isn't Haskell; pragmatism matters