Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions, emphasising pure functions that produce no side effects, immutable data that cannot be modified after creation, and function composition as the primary mechanism for building programs, rooted in Alonzo Church's lambda calculus (1936).
Functional programming treats your program as a series of transformations: data flows in, gets transformed by functions, and flows out — with no hidden changes to anything along the way.
What makes a function "pure"
A pure function has two properties. First: given the same inputs, it always returns the same output — no matter when you call it, no matter how many times, no matter what state the rest of the program is in. Second: it produces no side effects — it does not modify any variable outside itself, does not write to a file, does not print anything, does not change any shared state.
Contrast with an impure function:
# IMPURE: result depends on external state
total = 0
def add_to_total(x):
global total
total += x # side effect: modifies external state
return total # result depends on total, not just x
# PURE: same input always gives same output, no side effects
def add(x, y):
return x + y # always returns x + y, changes nothing
# add(3, 4) is always 7 — no matter what
# add_to_total(3) depends on what total currently is
Pure functions are predictable, testable, and parallelisable. You can test add(3, 4) in complete isolation — no setup required, no cleanup, no mocking. You can run it on eight CPU cores simultaneously — no shared state to conflict.
Immutability: data that doesn't change
In functional programming, once a value is created, it cannot be modified. Instead of changing a list, you create a new list with the change applied. Instead of updating a variable, you pass the new value to the next function.
This sounds wasteful, but efficient functional languages use persistent data structures — structures that share unchanged parts between the old and new version, so creating "a copy with one change" costs far less than copying the whole structure.
# Mutable style (imperative)
prices = [100, 200, 300]
for i in range(len(prices)):
prices[i] = prices[i] * 1.18 # modifies the list in place
# Immutable style (functional) — original list unchanged
prices = [100, 200, 300]
prices_with_gst = [p * 1.18 for p in prices] # new list, original intact
# prices is still [100, 200, 300]
Higher-order functions: functions that work with functions
In functional programming, functions are first-class values — you can pass them to other functions, return them from functions, and store them in variables. A higher-order function is one that takes another function as an argument or returns one.
Three higher-order functions appear in nearly every language:
map— apply a function to every element of a list, producing a new listfilter— keep only elements where a function returns truereduce— combine all elements into one value using a function
from functools import reduce
prices = [100, 200, 300, 50, 75]
# map: apply GST to every price
with_gst = list(map(lambda p: p * 1.18, prices))
# [118.0, 236.0, 354.0, 59.0, 88.5]
# filter: keep only prices over 100
expensive = list(filter(lambda p: p > 100, prices))
# [200, 300]
# reduce: sum all prices
total = reduce(lambda acc, p: acc + p, prices, 0)
# 725
Building programs from small pieces
Functional programs are built by composing small, pure functions into larger ones. The output of one function becomes the input of the next. Because each function is pure, you can reason about each piece independently and be confident the combination will work correctly.
This is the functional equivalent of the OOP principle of composition over inheritance — but at the function level rather than the object level.
Why functional ideas matter in every language
You don't need to use Haskell to benefit from functional programming ideas. Python, JavaScript, Java, Rust, Kotlin, and Go all support functional-style programming. Preferring pure functions, avoiding mutation where possible, and using map/filter instead of loops makes code in any language more testable and easier to reason about. Modern JavaScript and Python are written in a heavily functional style by many practitioners.
Functional languages
Pure functional: Haskell — all functions are pure; side effects (I/O, mutable state) are handled via a type-system mechanism (monads) that makes them explicit. Primarily functional: Erlang, Clojure, F#, Elm, Elixir. Multi-paradigm with strong functional support: Scala, Kotlin, Swift, Rust, JavaScript, Python.
Referential transparency
Referential transparency is the formal property that every pure function enjoys: an expression is referentially transparent if it can be replaced by its value without changing the program's behaviour. If add(3, 4) is always 7, you can replace every occurrence of add(3, 4) with 7 and the program behaves identically. This property enables reasoning about programs equationally — as you would in algebra. It also enables the compiler to cache results (memoisation), reorder calls, and parallelise evaluation.
Closures and lexical scope
A closure is a function that captures variables from its enclosing scope. Higher-order functions depend on closures to be practical:
def make_multiplier(n):
def multiply(x): # closes over n from the outer function
return x * n
return multiply # returns a function
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# double and triple are closures — they capture the value of n
Persistent data structures
Purely functional languages require data structures that support efficient "update without mutation." A persistent data structure preserves previous versions when modified. Clojure's persistent vectors use a 32-branch tree: "updating" an element at index i creates a new root node and a new path of log₃₂(n) nodes to the changed leaf, sharing all other nodes with the original. For a vector of 1 million elements, an update creates only 4 new nodes — O(log n) time and space, not O(n).
Currying and partial application
Currying (named after Haskell Curry) transforms a function of multiple arguments into a chain of single-argument functions: f(x, y) becomes f(x)(y). Every Haskell function is automatically curried. Partial application applies a function to some of its arguments, producing a new function waiting for the rest. Together, these make higher-order functions and composition more expressive.
// Curried add function
const add = x => y => x + y;
const add5 = add(5); // partial application — add5 waits for y
console.log(add5(3)); // 8
console.log(add5(10)); // 15
// Composing functions
const compose = (f, g) => x => f(g(x));
const double = x => x * 2;
const addTen = x => x + 10;
const doubleThenAdd = compose(addTen, double);
console.log(doubleThenAdd(5)); // 20 (5*2 = 10, 10+10 = 20)
Functional patterns in mainstream languages
Java 8+ introduced lambdas, streams (map/filter/reduce pipelines), and Optional (a functional way to handle nullable values). Kotlin has first-class function support, extension functions, and a rich collections API with functional operations. JavaScript's array methods (.map(), .filter(), .reduce()) and arrow functions enable functional style directly. React's design is explicitly functional: UI components are functions from state to view; state is immutable; updates produce new state.
map and filter. Those are functional-style tools, but the paradigm is about the properties of the functions themselves — purity and referential transparency. A program that uses map() with an impure function (one that writes to a database on each call) is not functional programming.const in JavaScript prevents the variable from being reassigned — but the object it points to can still be mutated. True immutability means the data structure itself cannot be modified. JavaScript's const arr = [1,2,3]; arr.push(4) is not immutable. Object.freeze() creates shallow immutability; libraries like Immutable.js or Immer provide deep immutability.Lambda calculus: the formal foundation
Alonzo Church introduced the lambda calculus in 1936 as a formal system for expressing computation through function abstraction and application. Lambda calculus has three constructs: variables (x), abstraction (λx.M — a function with parameter x and body M), and application (M N — apply function M to argument N).
Reduction rules: β-reduction applies a function: (λx.M)N → M[x := N] (substitute N for x in M). α-conversion renames bound variables to avoid capture. These two rules, applied repeatedly, compute the result of any lambda calculus expression. Church proved that lambda calculus is Turing-complete — any computable function can be expressed in lambda calculus, establishing that functions are sufficient for any computation (without needing mutation or sequencing).
Monads: making side effects explicit
A monad is a design pattern (formally: a type constructor M with two operations — return :: a → M a and bind :: M a → (a → M b) → M b — satisfying three monad laws: left identity, right identity, and associativity). Monads provide a structured way to sequence computations that have effects (IO, state, failure, non-determinism) while preserving referential transparency at the type level.
In Haskell, IO a is a type that describes a computation producing a value of type a as a side effect. A function of type IO String is not an impure function — it is a pure description of an I/O action. The main :: IO () function hands this description to the Haskell runtime, which executes it. Purity is preserved because the description is just a value; it is the runtime that performs the actual I/O.
Church, A. (1936). "An unsolvable problem of elementary number theory." American Journal of Mathematics, 58(2), 345–363. — Original lambda calculus paper. Hughes, J. (1989). "Why functional programming matters." The Computer Journal, 32(2), 98–107. — The canonical argument for the practical value of functional programming; motivates higher-order functions and lazy evaluation. Wadler, P. (1992). "The essence of functional programming." POPL '92. ACM. — Introduces monads to a programming audience.