Functional Programming
Building programs from pure functions and immutable data, avoiding shared state.
Overview
Functional Programming treats computation as the evaluation of mathematical functions, avoiding shared mutable state and side effects. Functions are first-class values; higher-order functions, immutability, and referential transparency are central. A function is referentially transparent if it can be replaced with its output value without changing program behaviour.
Origin
Rooted in Alonzo Church's lambda calculus (1930s). LISP (John McCarthy, 1958) was the first practical FP language. ML (1973) and Haskell (1990) formalised type systems. The 2000s saw adoption in mainstream languages: LINQ in C# (2007), Streams in Java 8 (2014), and pervasive use in JavaScript via lodash/ramda.
Examples
Composing pure transformations in TypeScript
type User = { id: number; name: string; age: number; active: boolean };
const users: User[] = [
{ id: 1, name: 'Alice', age: 32, active: true },
{ id: 2, name: 'Bob', age: 17, active: false },
{ id: 3, name: 'Carol', age: 25, active: true },
];
const activeAdults = (minAge: number) => (users: User[]) =>
users
.filter(u => u.active && u.age >= minAge)
.map(u => ({ id: u.id, name: u.name }))
.sort((a, b) => a.name.localeCompare(b.name));
const result = activeAdults(18)(users);
// [{ id: 1, name: 'Alice' }, { id: 3, name: 'Carol' }]activeAdults is a curried function returning a function. No mutation; each transformation returns a new array. The pipeline is easy to test by calling individual steps.
Reducing side effects with Maybe in Ruby
require 'dry-monads'
class UserRepository
include Dry::Monads[:maybe]
def find(id)
user = DB[:users].where(id: id).first
user ? Some(user) : None()
end
end
class UserService
def initialize(repo)
@repo = repo
end
def display_name(id)
@repo.find(id)
.fmap { |u| u[:name].upcase }
.value_or('Guest')
end
end
service = UserService.new(UserRepository.new)
puts service.display_name(1) # "ALICE"
puts service.display_name(99) # "Guest"dry-monads (v1.6+) provides Maybe, Result, and other monadic types. fmap applies the block only when the value is Some; None propagates without branching. Eliminates nil checks scattered throughout the codebase.
Use Cases
- 01Data pipeline construction (ETL, log processing) where each step is a pure transformation over a collection
- 02Concurrent systems where immutable data structures eliminate race conditions without locking
- 03Financial calculations requiring audit trails, where pure functions are trivially testable and free of side-effect surprises
- 04Frontend state management: Redux (2015) models application state as a pure function of previous state and an action
When Not to Use
- //I/O-heavy workflows where effects are unavoidable; wrapping everything in IO monads adds abstraction cost without benefit over simple async/await
- //Imperative algorithms (in-place sorting, graph traversal) that are naturally expressed with mutation and indexes
- //Teams unfamiliar with FP idioms; currying, point-free style, and monadic chaining have a steep reading curve for engineers coming from OOP backgrounds
Technical Notes
- Tail-call optimisation (TCO) is required for safe recursion in FP; Ruby does not implement TCO by default (MRI); Elixir and Haskell do. JavaScript (V8) implements TCO only in strict mode and only in specific syntax forms
- Persistent data structures (Clojure's PersistentVector, Immutable.js) achieve O(log n) updates by sharing structure between versions rather than copying; naive immutability via Object.freeze is O(n) on each change
- Referential transparency enables memoisation as a safe, automatic optimisation; React's useMemo and Reselect exploit this property directly
- Haskell's type system uses the Hindley-Milner algorithm (1978) to infer types without annotations; this underpins tools like TypeScript's contextual typing and OCaml's inference engine
More in Types of Programming