Functional programming treats computation as the evaluation of mathematical functions, avoiding mutable state and side effects. Programs are built from pure functions that always return the same output for the same input. Immutability simplifies reasoning and enables optimizations like memoization and parallel execution. Languages like Haskell, Lisp, and Scheme are primarily functional.
You have studied the lambda calculus — a formal system where computation is nothing more than defining and applying functions. Functional programming is what happens when you take that mathematical foundation seriously as a programming paradigm. Instead of writing programs as sequences of instructions that modify variables and memory (the imperative approach), you write programs as compositions of functions that take inputs and produce outputs without changing anything in the outside world. A function that adds two numbers always returns the same result for the same arguments, no matter when or how many times you call it. This property is called referential transparency, and it is the cornerstone of functional programming.
The most immediate practical consequence is immutability: variables are not boxes that hold changing values but names bound to fixed values. If you want a modified version of a list, you do not change the original — you create a new list with the modification applied. This seems wasteful at first, but it eliminates an entire category of bugs. In imperative code, a function might silently modify a shared data structure, causing a distant part of the program to break. In functional code, data cannot change after creation, so you never need to worry about who else might be modifying it. This makes programs easier to reason about, test, and run in parallel — if nothing is shared and mutable, threads cannot interfere with each other.
Functional programming relies heavily on higher-order functions — functions that take other functions as arguments or return them as results. You already know from lambda calculus that functions are values that can be passed around. In practice, this means operations like `map` (apply a function to every element of a collection), `filter` (keep elements satisfying a predicate), and `reduce` (combine elements using a binary operation) replace the loops and mutation of imperative code. Instead of writing a for-loop that modifies an accumulator variable, you express the transformation declaratively: `map(square, [1, 2, 3])` produces `[1, 4, 9]`. The pattern is to describe *what* you want computed, not the step-by-step *how*.
These properties have deep implications for compilers and language design. A compiler for a functional language can freely reorder, memoize, or parallelize function calls because pure functions have no side effects — the result depends only on the arguments. Lazy evaluation, used by Haskell, takes this further: expressions are not evaluated until their values are actually needed, which can avoid unnecessary computation and allows working with infinite data structures. The tradeoff is that functional programming requires a different way of thinking. Problems naturally expressed as state machines or in-place mutations (game loops, GUI event handling, database transactions) need careful encoding using techniques like monads or message-passing. But for data transformation pipelines, mathematical computation, and concurrent systems, the functional paradigm offers guarantees about correctness and composability that imperative code cannot easily match.
No topics depend on this one yet.