| 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.