| name | Functional Programming Fundamentals |
| description | Core FP concepts including pure functions, currying, composition, and pointfree style - the foundation for mastering functional TypeScript |
| version | 1.0.0 |
| author | Claude |
| tags | ["functional-programming","typescript","javascript","pure-functions","currying","composition","pointfree","fp-fundamentals"] |
Functional Programming Fundamentals
This skill covers the foundational concepts of functional programming. Master these before diving into fp-ts types like Option, Either, and Task. These concepts apply universally across functional programming languages and libraries.
Why Functional Programming?
Functional programming provides:
- Predictability: Pure functions always produce the same output for the same input
- Testability: No mocking of global state or dependencies
- Composability: Small functions combine into complex behavior
- Maintainability: Isolated pieces are easier to understand and change
- Parallelization: Pure functions can safely run concurrently
1. Pure Functions and Referential Transparency
What is a Pure Function?
A pure function has two properties:
- Same input, same output: Given the same arguments, it always returns the same result
- No side effects: It doesn't modify external state, perform I/O, or cause observable changes
Why It Matters
Pure functions are:
- Cacheable: Results can be memoized since they're deterministic
- Portable: No hidden dependencies on external state
- Testable: No setup/teardown of global state needed
- Parallelizable: Safe to run concurrently without race conditions
- Reasoned about locally: You can understand them without knowing the entire system
Examples
const add = (x: number, y: number): number => x + y;
const greet = (name: string): string => `Hello, ${name}!`;
const head = <T>(arr: readonly T[]): T | undefined => arr[0];
let counter = 0;
const incrementCounter = (): number => {
counter += 1;
return counter;
};
const randomNumber = (): number => Math.random();
const logMessage = (msg: string): void => {
console.log(msg);
};
const config = { apiUrl: "https://api.example.com" };
const getApiUrl = (): string => config.apiUrl;
Referential Transparency
An expression is referentially transparent if it can be replaced with its value without changing program behavior.
const double = (x: number): number => x * 2;
const result = double(5) + double(5);
let count = 0;
const incrementAndGet = (): number => {
count += 1;
return count;
};
const result2 = incrementAndGet() + incrementAndGet();
Making Impure Functions Pure
const addItem = (arr: string[], item: string): void => {
arr.push(item);
};
const addItem = (arr: readonly string[], item: string): readonly string[] => [
...arr,
item,
];
const getAge = (birthYear: number): number =>
new Date().getFullYear() - birthYear;
const getAge = (birthYear: number, currentYear: number): number =>
currentYear - birthYear;
const getUser = async (id: string): Promise<User> => await database.find(id);
const getUser =
(id: string): Task<User> =>
() =>
database.find(id);
Common Mistakes
const sortArray = (arr: number[]): number[] => {
return arr.sort((a, b) => a - b);
};
const sortArray = (arr: readonly number[]): number[] =>
[...arr].sort((a, b) => a - b);
const generateId = (): string => `id-${Date.now()}-${Math.random()}`;
const generateId = (timestamp: number, random: number): string =>
`id-${timestamp}-${random}`;
const multiplier = 2;
const double = (x: number): number => x * multiplier;
const multiply =
(factor: number) =>
(x: number): number =>
x * factor;
const double = multiply(2);
2. Currying and Partial Application
What is Currying?
Currying transforms a function that takes multiple arguments into a sequence of functions that each take a single argument.
const add = (x: number, y: number, z: number): number => x + y + z;
add(1, 2, 3);
const addCurried =
(x: number) =>
(y: number) =>
(z: number): number =>
x + y + z;
addCurried(1)(2)(3);
Why It Matters
Currying enables:
- Partial application: Create specialized functions by supplying some arguments
- Composition: Single-argument functions compose cleanly
- Reusability: Create families of related functions from one definition
- Deferred computation: Supply arguments as they become available
Currying in Practice
const multiply =
(x: number) =>
(y: number): number =>
x * y;
const double = multiply(2);
const triple = multiply(3);
double(5);
triple(5);
const greaterThan =
(threshold: number) =>
(value: number): boolean =>
value > threshold;
const isAdult = greaterThan(17);
const isExpensive = greaterThan(100);
isAdult(25);
isExpensive(50);
Currying for Array Methods
const filterBy =
<T>(predicate: (item: T) => boolean) =>
(items: readonly T[]): T[] =>
items.filter(predicate);
const keepPositive = filterBy((n: number) => n > 0);
const keepEven = filterBy((n: number) => n % 2 === 0);
keepPositive([-1, 2, -3, 4]);
keepEven([1, 2, 3, 4, 5, 6]);
const mapWith =
<A, B>(fn: (a: A) => B) =>
(items: readonly A[]): B[] =>
items.map(fn);
const doubleAll = mapWith((n: number) => n * 2);
const stringify = mapWith(String);
doubleAll([1, 2, 3]);
stringify([1, 2, 3]);
Partial Application vs Currying
const greet = (greeting: string, name: string): string =>
`${greeting}, ${name}!`;
const sayHello = greet.bind(null, "Hello");
sayHello("Alice");
const greetCurried =
(greeting: string) =>
(name: string): string =>
`${greeting}, ${name}!`;
const sayHello = greetCurried("Hello");
const sayGoodbye = greetCurried("Goodbye");
sayHello("Alice");
sayGoodbye("Bob");
Argument Order Matters
const map = <A, B>(arr: A[], fn: (a: A) => B): B[] => arr.map(fn);
const doubleAll = (arr: number[]) => map(arr, (n) => n * 2);
const map =
<A, B>(fn: (a: A) => B) =>
(arr: readonly A[]): B[] =>
arr.map(fn);
const doubleAll = map((n: number) => n * 2);
const stringify = map(String);
import * as A from "fp-ts/Array";
import { pipe } from "fp-ts/function";
pipe(
[1, 2, 3],
A.map((n) => n * 2),
A.filter((n) => n > 2)
);
Common Mistakes
const process = (config: Config) => (data: Data) => (options: Options) => ...
const handle = (data: Data) => (config: Config) => ...
const process = (config: Config) => (options: Options) => (data: Data) => ...
const handle = (config: Config) => (data: Data) => ...
const add = (a: number) => (b: number) => (c: number) => (d: number) =>
a + b + c + d
add(1)(2)(3)(4)
const add = (a: number, b: number, c: number, d: number) => a + b + c + d
const addPairs = (a: number, b: number) => (c: number, d: number) =>
a + b + c + d
const formatPrice = (currency: string, amount: number): string =>
`${currency}${amount.toFixed(2)}`
pipe(
100,
formatPrice('$', ???)
)
const formatPrice = (currency: string) => (amount: number): string =>
`${currency}${amount.toFixed(2)}`
pipe(
100,
formatPrice('$')
)
3. Function Composition
What is Function Composition?
Function composition combines two or more functions to create a new function. The output of one function becomes the input of the next.
const compose =
<A, B, C>(f: (b: B) => C, g: (a: A) => B) =>
(x: A): C =>
f(g(x));
const addOne = (x: number): number => x + 1;
const double = (x: number): number => x * 2;
const addOneThenDouble = compose(double, addOne);
addOneThenDouble(5);
Why It Matters
Composition enables:
- Building complex behavior from simple pieces: Small, focused functions combine into powerful transformations
- Reusability: Composed functions can themselves be composed further
- Readability: Each step has a clear, single purpose
- Testing: Test small pieces in isolation, composition is mathematically guaranteed
Pipe vs Flow (Left-to-Right Composition)
Traditional mathematical composition reads right-to-left, which can be confusing. pipe and flow provide left-to-right composition.
import { pipe, flow } from "fp-ts/function";
const addOne = (x: number): number => x + 1;
const double = (x: number): number => x * 2;
const toString = (x: number): string => `Value: ${x}`;
const result = pipe(
5,
addOne,
double,
toString
);
const transform = flow(addOne, double, toString);
transform(5);
transform(10);
Composition with Multiple Types
const parseNumber = (s: string): number => parseInt(s, 10);
const isEven = (n: number): boolean => n % 2 === 0;
const toYesNo = (b: boolean): string => (b ? "Yes" : "No");
const isInputEven = flow(
parseNumber,
isEven,
toYesNo
);
isInputEven("4");
isInputEven("7");
Building Pipelines
import { pipe, flow } from "fp-ts/function";
interface User {
name: string;
email: string;
age: number;
}
const getName = (user: User): string => user.name;
const toUpperCase = (s: string): string => s.toUpperCase();
const addGreeting = (name: string): string => `Hello, ${name}!`;
const addExcitement = (s: string): string => `${s}!!!`;
const greetUser = flow(getName, toUpperCase, addGreeting, addExcitement);
greetUser({ name: "alice", email: "alice@example.com", age: 30 });
const greeting = pipe(
{ name: "bob", email: "bob@example.com", age: 25 },
getName,
toUpperCase,
addGreeting
);
Associativity of Composition
Composition is associative: (f . g) . h = f . (g . h)
const f = (x: number): number => x + 1;
const g = (x: number): number => x * 2;
const h = (x: number): number => x - 3;
const way1 = flow(flow(h, g), f);
const way2 = flow(h, flow(g, f));
const way3 = flow(h, g, f);
way1(10);
way2(10);
way3(10);
Common Mistakes
const toString = (n: number): string => String(n);
const double = (n: number): number => n * 2;
const bad = flow(toString, double);
const add = (a: number, b: number): number => a + b;
const double = (n: number): number => n * 2;
const bad = flow(add, double);
const add =
(a: number) =>
(b: number): number =>
a + b;
const addFiveThenDouble = flow(add(5), double);
addFiveThenDouble(3);
const result = pipe(
data,
fn1,
fn2,
fn3,
fn4,
fn5,
fn6,
fn7,
fn8,
fn9,
fn10
);
const parseInput = flow(fn1, fn2, fn3);
const validateData = flow(fn4, fn5);
const formatOutput = flow(fn6, fn7, fn8, fn9, fn10);
const process = flow(parseInput, validateData, formatOutput);
4. Pointfree Style
What is Pointfree Style?
Pointfree (or tacit) programming defines functions without explicitly mentioning their arguments. The "points" are the arguments.
const double = (x: number): number => x * 2;
const double = multiply(2);
Why It Matters
Pointfree style:
- Reduces noise: Focuses on transformations, not data shuffling
- Encourages composition: Works naturally with flow/pipe
- Reveals patterns: Makes the structure of computation visible
- Prevents mistakes: No variable names to misspell or shadow
Pointfree in Practice
import { pipe, flow } from "fp-ts/function";
import * as A from "fp-ts/Array";
const getActiveUserNames = (users: User[]): string[] =>
users.filter((user) => user.isActive).map((user) => user.name);
const isActive = (user: User): boolean => user.isActive;
const getName = (user: User): string => user.name;
const getActiveUserNames = flow(A.filter(isActive), A.map(getName));
getActiveUserNames(users);
Building Pointfree Functions
const prop =
<T, K extends keyof T>(key: K) =>
(obj: T): T[K] =>
obj[key];
const equals =
<T>(target: T) =>
(value: T): boolean =>
value === target;
const gt =
(threshold: number) =>
(value: number): boolean =>
value > threshold;
interface Product {
name: string;
price: number;
category: string;
}
const getExpensiveElectronics = (products: Product[]): Product[] =>
products.filter((p) => p.category === "electronics" && p.price > 100);
const isElectronics = flow(
prop<Product, "category">("category"),
equals("electronics")
);
const isExpensive = flow(prop<Product, "price">("price"), gt(100));
const both =
<T>(f: (t: T) => boolean, g: (t: T) => boolean) =>
(t: T): boolean =>
f(t) && g(t);
const getExpensiveElectronics = A.filter(both(isElectronics, isExpensive));
When Pointfree Helps
const doubleAll = A.map((n: number) => n * 2);
const keepPositive = A.filter((n: number) => n > 0);
const double = (n: number): number => n * 2;
const isPositive = (n: number): boolean => n > 0;
const doubleAll = A.map(double);
const keepPositive = A.filter(isPositive);
const processNumbers = flow(keepPositive, doubleAll);
When to Avoid Pointfree
const mystery = flow(
A.map(flow(prop("x"), add(1))),
A.filter(flow(prop("y"), gt(5))),
A.reduce(
0,
flow(([acc, item]) => acc + item.z)
)
);
const incrementX = (item: Item): Item => ({ ...item, x: item.x + 1 });
const hasLargeY = (item: Item): boolean => item.y > 5;
const sumZ = (items: Item[]): number =>
items.reduce((acc, item) => acc + item.z, 0);
const process = flow(A.map(incrementX), A.filter(hasLargeY), sumZ);
const weird = flip(compose(flip(map), filter))(isEven)(double);
const doubleEvens = flow(A.filter(isEven), A.map(double));
Common Mistakes
const process = flow(A.map(flow(juxt([prop("a"), prop("b")]), apply(add))));
const process = A.map(({ a, b }) => a + b);
const double = A.map((n) => n * 2);
const double = A.map((n: number) => n * 2);
const process = (arr: number[]) =>
flow(
A.filter((n) => n > 0),
A.map((n) => n * 2)
)(arr);
const process = flow(
A.filter((n: number) => n > 0),
A.map((n) => n * 2)
);
const process = (arr: number[]): number[] =>
pipe(
arr,
A.filter((n) => n > 0),
A.map((n) => n * 2)
);
5. First-Class Functions
What are First-Class Functions?
In JavaScript/TypeScript, functions are first-class citizens. They can be:
- Assigned to variables
- Passed as arguments
- Returned from other functions
- Stored in data structures
Why It Matters
First-class functions enable:
- Higher-order functions: Functions that take or return functions
- Callbacks: Passing behavior as data
- Closures: Functions that capture their environment
- Functional composition: Building programs from function combinations
Avoiding Wrapper Functions
A common anti-pattern is wrapping functions unnecessarily.
const numbers = [1, 2, 3, 4, 5];
numbers.map((x) => double(x));
numbers.filter((x) => isPositive(x));
numbers.forEach((x) => console.log(x));
numbers.map(double);
numbers.filter(isPositive);
numbers.forEach(console.log);
When Wrappers ARE Needed
const users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
];
users.map((user, index) => `${index}: ${user.name}`);
const fetchAll = (urls: string[]) => urls.map((url) => fetch(url));
const add = (a: number, b: number): number =>
a + b[(1, 2, 3)].map((n) => add(n, 10));
const add =
(b: number) =>
(a: number): number =>
a + b[(1, 2, 3)].map(add(10));
Higher-Order Functions
const multiply =
(x: number) =>
(y: number): number =>
x * y;
const double = multiply(2);
const triple = multiply(3);
const applyTwice =
<T>(fn: (t: T) => T) =>
(value: T): T =>
fn(fn(value));
const addOne = (n: number): number => n + 1;
const addTwo = applyTwice(addOne);
addTwo(5);
const compose =
<A, B, C>(f: (b: B) => C) =>
(g: (a: A) => B) =>
(x: A): C =>
f(g(x));
const increment = (n: number): number => n + 1;
const toString = (n: number): string => String(n);
const incrementThenStringify = compose(toString)(increment);
incrementThenStringify(5);
Method References
const obj = {
value: 42,
getValue() {
return this.value;
},
};
const getValue = obj.getValue;
getValue();
const getValue = obj.getValue.bind(obj);
getValue();
const getValue = () => obj.getValue();
getValue();
const getValue = (obj: { value: number }): number => obj.value;
getValue({ value: 42 });
Common Mistakes
const names = users.map((user) => getName(user));
const names = users.map(getName);
const numbers = ["1", "2", "3"].map((s) => parseInt(s));
const numbers = ["1", "2", "3"].map((s) => parseInt(s, 10));
const numbers = ["1", "2", "3"].map(Number);
const process = (items: Item[], transform: (item: Item) => Item) => {
const result = [];
for (const item of items) {
result.push(transform(item));
}
return result;
};
const process = (items: Item[], transform: (item: Item) => Item) =>
items.map(transform);
items.map(transform);
const createAdder = (x: number) => (y: number) => x + y;
const add5 = createAdder(5)();
const add5 = createAdder(5);
add5(3);
Practical Exercises
Exercise 1: Pure Functions
Identify what makes these functions impure and rewrite them as pure functions.
const fetchUser = async (id: string) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};
const isExpired = (expiryDate: Date) => {
return expiryDate < new Date();
};
const addToCart = (cart: CartItem[], item: CartItem) => {
cart.push(item);
return cart;
};
const config = { taxRate: 0.08 };
const calculateTotal = (subtotal: number) => {
return subtotal * (1 + config.taxRate);
};
Solutions
type FetchUser = (id: string) => () => Promise<User>;
const fetchUser: FetchUser = (id) => () =>
fetch(`/api/users/${id}`).then((r) => r.json());
const isExpired = (expiryDate: Date, now: Date): boolean => expiryDate < now;
const addToCart = (cart: readonly CartItem[], item: CartItem): CartItem[] => [
...cart,
item,
];
const calculateTotal =
(taxRate: number) =>
(subtotal: number): number =>
subtotal * (1 + taxRate);
interface TaxConfig {
taxRate: number;
}
const calculateTotal = (config: TaxConfig, subtotal: number): number =>
subtotal * (1 + config.taxRate);
Exercise 2: Currying and Partial Application
Convert these functions to curried form and create specialized versions.
const formatDate = (
locale: string,
options: Intl.DateTimeFormatOptions,
date: Date
): string => date.toLocaleDateString(locale, options);
const clamp = (min: number, max: number, value: number): number =>
Math.max(min, Math.min(max, value));
const hasProperty = (obj: object, key: string): boolean => key in obj;
Solutions
const formatDate =
(locale: string) =>
(options: Intl.DateTimeFormatOptions) =>
(date: Date): string =>
date.toLocaleDateString(locale, options);
const formatUSDate = formatDate("en-US");
const formatShortDate = formatUSDate({ month: "short", day: "numeric" });
formatShortDate(new Date());
const clamp =
(min: number) =>
(max: number) =>
(value: number): number =>
Math.max(min, Math.min(max, value));
const clampPercentage = clamp(0)(100);
const clampByte = clamp(0)(255);
clampPercentage(150);
clampByte(-10);
const hasProperty =
(key: string) =>
(obj: object): boolean =>
key in obj;
const hasEmail = hasProperty("email");
const hasId = hasProperty("id");
users.filter(hasEmail);
Exercise 3: Function Composition
Build these pipelines using flow and pipe.
import { pipe, flow } from "fp-ts/function";
import * as A from "fp-ts/Array";
interface Product {
name: string;
price: number;
category: string;
inStock: boolean;
}
Solutions
const getAffordableInStockNames = (products: Product[]): string[] =>
pipe(
products,
A.filter((p) => p.inStock),
A.filter((p) => p.price < 50),
A.map((p) => p.name),
A.sort((a, b) => a.localeCompare(b))
);
const getAffordableInStockNames = flow(
A.filter((p: Product) => p.inStock),
A.filter((p) => p.price < 50),
A.map((p) => p.name),
A.sort<string>((a, b) => a.localeCompare(b))
);
const slugify = flow(
(s: string) => s.trim(),
(s) => s.toLowerCase(),
(s) => s.replace(/\s+/g, "-"),
(s) => `slug-${s}`
);
slugify(" Hello World ");
const isPositive = (n: number): boolean => n >= 0;
const double = (n: number): number => n * 2;
const sum = (numbers: number[]): number =>
numbers.reduce((acc, n) => acc + n, 0);
const formatTotal = (n: number): string => `Total: ${n}`;
const processNumbers = flow(
A.filter(isPositive),
A.map(double),
sum,
formatTotal
);
processNumbers([-1, 2, 3, -4, 5]);
Exercise 4: Pointfree Style
Refactor these functions to pointfree style where appropriate.
const getAdultNames = (users: User[]): string[] =>
users.filter((user) => user.age >= 18).map((user) => user.name);
const sumPrices = (products: Product[]): number =>
products.reduce((total, product) => total + product.price, 0);
const formatUserForDisplay = (user: User): string =>
`${user.name} (${user.email}) - ${user.isActive ? "Active" : "Inactive"}`;
Solutions
const isAdult = (user: User): boolean => user.age >= 18;
const getName = (user: User): string => user.name;
const getAdultNames = flow(A.filter(isAdult), A.map(getName));
const prop =
<T, K extends keyof T>(key: K) =>
(obj: T): T[K] =>
obj[key];
const sum = (numbers: number[]): number => numbers.reduce((a, b) => a + b, 0);
const sumPrices = flow(A.map(prop<Product, "price">("price")), sum);
const getPrice = (p: Product): number => p.price;
const sumPrices = flow(A.map(getPrice), sum);
const formatUserForDisplay = (user: User): string =>
`${user.name} (${user.email}) - ${user.isActive ? "Active" : "Inactive"}`;
const formatUserForDisplay = flow(
(user) => [user.name, user.email, user.isActive] as const,
([name, email, isActive]) =>
`${name} (${email}) - ${isActive ? "Active" : "Inactive"}`
);
Exercise 5: First-Class Functions
Fix these unnecessary wrappers and improve the code.
const results = items
.map((item) => processItem(item))
.filter((result) => isValid(result))
.forEach((result) => logResult(result));
const numbers = ["1", "2", "3", "10", "11"].map(parseInt);
const handlers = {
onClick: (e: Event) => handleClick(e),
onSubmit: (e: Event) => handleSubmit(e),
onHover: (e: Event) => handleHover(e),
};
Solutions
const results = items.map(processItem).filter(isValid).forEach(logResult);
const numbers = ["1", "2", "3", "10", "11"].map((s) => parseInt(s, 10));
const numbers = ["1", "2", "3", "10", "11"].map(Number);
const handlers = {
onClick: handleClick,
onSubmit: handleSubmit,
onHover: handleHover,
};
const createHandlers = <T extends Record<string, (e: Event) => void>>(
handlerMap: T
): T => handlerMap;
const handlers = createHandlers({
onClick: handleClick,
onSubmit: handleSubmit,
onHover: handleHover,
});
Summary
| Concept | Key Idea | Benefit |
|---|
| Pure Functions | Same input = same output, no side effects | Predictable, testable, cacheable |
| Currying | Transform multi-arg to single-arg chain | Partial application, composition |
| Composition | Combine small functions into larger ones | Reusability, modularity |
| Pointfree | Define functions without naming arguments | Less noise, reveals structure |
| First-Class Functions | Functions as values | Higher-order functions, callbacks |
Next Steps
With these fundamentals mastered, you're ready for:
- fp-ts Option and Either: Functional error handling
- fp-ts Pipe and Flow: Advanced composition patterns
- fp-ts Task and TaskEither: Async functional programming
- Monads and Functors: The algebraic structures behind fp-ts
Remember: FP is about building complex behavior from simple, composable pieces. Start small, practice composition, and gradually adopt more advanced patterns as they prove useful in your code.