Functional Programming Overview
Functional programming treats computation as the evaluation of mathematical functions, avoiding mutable state and side effects. It emphasises functions as first-class citizens and composition over inheritance.
Core Concepts
Pure Functions
A pure function always produces the same output for the same input and has no side effects. Pure functions are easier to reason about, test, and debug:
def add(x, y):
return x + y # pure; no external state modifiedAn impure function modifies external state or relies on mutable input. In C++, a function that modifies a reference parameter is impure:
void increment(int &value) {
value += 1; // modifies external state
}Immutability
Data structures are not modified in place. Instead, new structures are created with the desired changes. This eliminates entire classes of bugs caused by shared mutable state:
original = [1, 2, 3]
new_list = original + [4] # original unchangedIn C++, const provides immutability at the variable level. In Rust, all
variables are immutable by default — see
Learning Rust Basics for how ownership
enforces this.
First-Class and Higher-Order Functions
Functions are first-class citizens: they can be assigned to variables, passed as arguments, and returned from other functions. A higher-order function takes or returns functions:
def apply_function(func, value):
return func(value)
result = apply_function(lambda x: x + 1, 5) # 6Function Composition
Small functions are combined to build complex behaviour:
h = g(f(x))In Rust, closures and iterators compose naturally through method chaining:
let result: Vec<i32> = numbers.iter().map(|x| x * 2).filter(|x| x > 5).collect();Closures
Closures capture variables from their enclosing lexical scope:
def make_multiplier(factor):
def multiplier(x):
return x * factor
return multiplier
double = make_multiplier(2)
double(5) # 10Recursion
Recursion replaces iterative loops. A base case terminates the recursion; the recursive case breaks the problem down:
def factorial(n):
if n == 0:
return 1 # base case
return n * factorial(n - 1) # recursive caseLazy Evaluation
Expressions are evaluated only when their value is needed. This enables infinite data structures and avoids unnecessary computation:
def infinite_numbers():
n = 0
while True:
yield n
n += 1Referential Transparency
An expression is referentially transparent if it can be replaced by its value without changing the program’s behaviour. Pure functions are referentially transparent; impure functions are not.
Functional Programming in Different Languages
C++
C++ supports FP through std::function, lambda expressions (C++11), and STL
algorithms like std::for_each, std::transform, and std::copy_if:
#include <algorithm>
#include <vector>
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::for_each(numbers.begin(), numbers.end(), [](int &x) { x *= 2; });Python
Python provides built-in map, filter, reduce, and supports closures and
generators:
doubled = list(map(lambda x: x * 2, numbers))Rust
Rust provides iterators, closures, and map/filter/collect chains with
strong type safety and no runtime cost:
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();Advantages
- Modularity: Small composable functions.
- Testability: Pure functions need no mocking.
- Concurrency: Immutability eliminates data races.
- Maintainability: Less accidental state entanglement.
Disadvantages
- Performance overhead: New allocations instead of in-place mutation.
- Learning curve: Mindset shift from imperative to declarative.
- Library support: Fewer FP-optimised libraries in mainstream ecosystems.