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 modified

An 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 unchanged

In 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)  # 6

Function 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)  # 10

Recursion

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 case

Lazy 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 += 1

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

References