Functional Programming (FP)

Functional programming emphasizes functions, expression evaluation, composition, immutability, and controlled side effects.

5.1 Core idea

Functional programming emphasizes functions, expression evaluation, composition, immutability, and controlled side effects. Pure functional languages restrict effects more strongly; multi-paradigm languages allow functional techniques alongside mutation and imperative code.

5.2 Pure functions

For the same inputs, this function returns the same result and does not change external state. Production code should also validate domain constraints such as non-negative prices and permitted discount rates.

function discount(price: number, rate: number): number {
  return price * (1 - rate);
}

5.3 Immutability

Instead of modifying existing data, create a new value.

The spread expression creates a shallow copy. Nested objects remain shared unless copied or modeled immutably as well. Immutability reduces accidental state changes but can have allocation costs.

type User = { id: string; name: string; active: boolean };

const user: User = { id: "u1", name: "Asha", active: true };
const deactivatedUser = { ...user, active: false };

5.4 First-class and higher-order functions

A language treats functions as first-class values when they can be assigned, passed, returned, and stored like other values. A higher-order function accepts a function or returns one.

Output (Node.js 24.18) — note that Node prints arrays with spaces inside the brackets:

Use map for transformation, filter for selection, and reduce for aggregation. Do not use a reduction when a simpler operation communicates intent better.

const values = [1, 2, 3, 4];

const doubled = values.map(x => x * 2);
const even = values.filter(x => x % 2 === 0);
const total = values.reduce((sum, x) => sum + x, 0);

console.log(doubled); // [ 2, 4, 6, 8 ]
console.log(even);    // [ 2, 4 ]
console.log(total);   // 10
[ 2, 4, 6, 8 ]
[ 2, 4 ]
10

5.5 Function composition

Composition combines smaller functions into a larger function.

Output (Node.js 24.18, TypeScript run with type stripping):

This example normalizes whitespace and case for demonstration; real email identity rules can be more nuanced and should follow the application's account policy.

A functional transformation pipeline Text equivalent: normalizeEmail is the composition of two pure functions. The input string is never modified; each step returns a new string. The real Node.js run printed user@example.com.

const trim = (s: string) => s.trim();
const lowercase = (s: string) => s.toLowerCase();
const normalizeEmail = (s: string) => lowercase(trim(s));

console.log(normalizeEmail("  USER@EXAMPLE.COM "));
// user@example.com
user@example.com
flowchart LR
  A["'  USER@EXAMPLE.COM '"] -->|trim| B["'USER@EXAMPLE.COM'"]
  B -->|lowercase| C["'user@example.com'"]

5.6 Closures

A closure retains access to variables from its lexical environment.

Output (Node.js 24.18): The returned function keeps count alive. Closures are useful for callbacks, factories, and encapsulated state, but retained references can also extend object lifetimes unexpectedly.

function makeCounter() {
  let count = 0;
  return function increment() {
    count += 1;
    return count;
  };
}

const next = makeCounter();
console.log(next()); // 1
console.log(next()); // 2

5.7 Recursion and tail recursion

Recursion expresses a problem in terms of smaller subproblems. This implementation is educational; large inputs may exceed the recursion limit or overflow fixed-width integer types in other languages. Tail-call optimization is language/compiler dependent and must not be assumed universally.

def factorial(n: int) -> int:
    if n < 0:
        raise ValueError("n must be non-negative")
    if n <= 1:
        return 1
    return n * factorial(n - 1)

5.8 Algebraic data types and pattern matching

_Not compiled — no Rust toolchain on the build machine._ An algebraic data type can represent a value as one of several explicit cases. Pattern matching makes cases visible and can let the compiler identify missing branches.

enum PaymentStatus {
    Pending,
    Paid { transaction_id: String },
    Failed { reason: String },
}

fn describe(status: PaymentStatus) -> String {
    match status {
        PaymentStatus::Pending => "Pending".to_string(),
        PaymentStatus::Paid { transaction_id } =>
            format!("Paid: {transaction_id}"),
        PaymentStatus::Failed { reason } =>
            format!("Failed: {reason}"),
    }
}

5.9 Option/Maybe and Result/Either

Use explicit types to represent absence and failure rather than confusing a missing value with a valid value.

_Not compiled — a fragment: User is defined elsewhere and todo!() marks the missing body._

Option<T> represents presence or absence. Result<T, E> represents success or failure with an error. Equivalent concepts exist in many languages under different names. Avoid using exceptions or null values inconsistently where explicit error modeling improves clarity.

fn find_user(id: &str) -> Option<User> {
    // Some(user) when found; None when absent.
    todo!()
}

5.10 Functional effects

Real programs need files, networks, clocks, databases, and user input. Functional designs often isolate effects at boundaries and keep core transformations pure.

Functional core, imperative shell: Core: deterministic business rules and transformations.

Shell: I/O, framework calls, database access, clocks, and other effects.

This makes core behavior easier to test without requiring every test to start external services.

5.11 Lazy evaluation

Lazy evaluation delays computation until the result is needed. It can enable infinite sequences and avoid unnecessary work, but it can also create surprising performance, memory, and evaluation-order behavior.

_Not run — no Haskell compiler on the build machine._ This Haskell expression describes an unbounded sequence and takes its first ten values. Laziness means the entire sequence need not be materialized.

naturals = [0..]
firstTen = take 10 naturals

5.12 Functional programming concepts to explore

Currying and partial application Fold, map, filter, flatMap/bind Persistent data structures Algebraic data types Type classes and traits Monads, applicatives, and functors Effect systems and effect handlers Functional reactive programming

Referential equality versus structural equality Strictness and laziness Memoization and caching

Recursion schemes and folds over recursive structures

Exercise

Implement a shopping-cart total using (1) a loop and mutable accumulator, (2) map plus reduce, and (3) a pure function pipeline. Add tests for empty carts, discounts, rounding, and invalid quantities.

Quiz

? Which function is pure?
+ `discount(price, rate) => price * (1 - rate)` :: Same inputs always give the same output, and nothing outside changes.
- A function that reads the current time :: Its result depends on hidden input (the clock).
- A function that appends to a global list :: It mutates external state.
- A function that prints its result :: Printing is a side effect.

? `const deactivatedUser = { ...user, active: false }` — what is true? (select all that apply)
+ It creates a new object; `user` is unchanged. :: The spread copies the properties into a new object.
+ The copy is shallow; nested objects are still shared. :: Only the top level is copied.
- It deep-clones every nested object. :: Spread does not deep-copy.
- It mutates `user.active`. :: The original object is not modified.

? What does `makeCounter()` demonstrate when `next()` returns 1 and then 2?
- Recursion :: There is no self-call.
+ A closure keeping `count` alive between calls :: The returned function retains its lexical environment.
- Lazy evaluation :: Nothing is delayed.
- Tail-call optimisation :: There is no tail call.