kae3g 9520: Functional Programming - Computing with Pure Functions
Phase 1: Foundations & Philosophy | Week 2 | Reading Time: 16 minutes
What You'll Learn
- What functional programming (FP) actually means
- Pure functions: same input → same output, no side effects
- Immutability: why unchanging data prevents bugs
- Higher-order functions: functions that take/return functions
- Composition: building complex behavior from simple pieces
- Why FP is becoming dominant (concurrency, testability, reasoning)
- How FP relates to mathematics (lambda calculus, category theory)
Prerequisites
- 9504: What Is Clojure? - Practical FP example
- 9510: Unix Philosophy - Composition mindset
What Is Functional Programming?
The simple definition:
Functional programming treats computation as the evaluation of mathematical functions and avoids changing state and mutable data.
Unpacking:
- Computation = function evaluation (not sequences of instructions)
- Avoid changing state (no x = x + 1)
- Avoid mutable data (values don't change after creation)
The shift:
Imperative: "Do this, then do that, then do this other thing"
            (recipe—list of steps)
Functional: "The answer is f(g(h(input)))"
            (equation—composition of functions)
Pure Functions: The Foundation
A pure function:
- Same input → same output (deterministic, no randomness)
- No side effects (doesn't change anything outside itself)
Examples
Pure:
(defn add [x y]
  (+ x y))
(add 3 5)  ; => 8
(add 3 5)  ; => 8 (always!)
- Same inputs (3, 5) → same output (8)
- No side effects (doesn't print, doesn't write files, doesn't change global state)
Impure (side effect: printing):
(defn add-and-log [x y]
  (println "Adding" x "and" y)  ; SIDE EFFECT!
  (+ x y))
(add-and-log 3 5)  
; Prints: "Adding 3 and 5"
; => 8
- Still returns same output, but observable effect (printing)
- Not mathematically pure
Impure (side effect: mutation):
let total = 0;
function addToTotal(x) {
  total = total + x;  // SIDE EFFECT: mutates global
  return total;
}
addToTotal(5)  ; => 5
addToTotal(5)  ; => 10 (different output for same input!)
- Different outputs for same input
- Non-deterministic (output depends on when you call it)
- Hard to test (must set up global state)
Why Purity Matters
Benefits of pure functions:
- Easy to test;; Pure function: just call it (add 3 5) ; => 8 (test passed!) ;; Impure function: must set up state, mock I/O, etc.
- Easy to reason about;; What does this do? (defn mystery [x] (* x x)) ;; Just look at the code: squares its input ;; No need to check global variables, database state, etc.
- Parallel-safe;; Call these in parallel—no race conditions! (future (add 1 2)) (future (add 3 4)) (future (add 5 6))
- Memoizable (cache results);; Since (expensive-calc 42) always returns same value, ;; compute once, cache forever (def expensive-calc (memoize expensive-calc))
- Time-travel debugging;; Replay with same inputs → same outputs (reproducible bugs!)
Immutability: Values That Never Change
Most languages: Variables vary.
x = [1, 2, 3]
x.append(4)  # Mutated!
# x is now [1, 2, 3, 4]
FP languages: Values are immutable.
(def x [1 2 3])
(def y (conj x 4))  ; New value
x  ; => [1 2 3] (unchanged!)
y  ; => [1 2 3 4] (new value)
Why Immutability Prevents Bugs
The problem with mutation:
function processUsers(users) {
  users.sort((a, b) => a.age - b.age);  // MUTATES input!
  return users.filter(u => u.age > 18);
}
const allUsers = [{name: "Alice", age: 30}, {name: "Bob", age: 17}];
const adults = processUsers(allUsers);
// allUsers is now SORTED (side effect!)
// Other code expecting original order: BROKEN
With immutability:
(defn process-users [users]
  (->> users
       (sort-by :age)
       (filter #(> (:age %) 18))))
(def all-users [{:name "Alice" :age 30} {:name "Bob" :age 17}])
(def adults (process-users all-users))
;; all-users is UNCHANGED (no side effect)
;; Other code: SAFE
Immutability = no spooky action at a distance.
Higher-Order Functions
Functions that take functions as arguments or return functions as results.
Map: Transform Each Element
(map inc [1 2 3 4 5])
; => (2 3 4 5 6)
(map square [1 2 3 4 5])
; => (1 4 9 16 25)
(map :name [{:name "Alice"} {:name "Bob"}])
; => ("Alice" "Bob")
map is a higher-order function (takes function inc, square, :name as argument).
Filter: Keep Some Elements
(filter even? [1 2 3 4 5 6])
; => (2 4 6)
(filter #(> % 10) [5 15 3 20 7 30])
; => (15 20 30)
filter is higher-order (takes predicate function as argument).
Reduce: Combine Elements
(reduce + [1 2 3 4 5])
; => 15  (1+2+3+4+5)
(reduce * [1 2 3 4 5])
; => 120 (1*2*3*4*5 = 5 factorial)
(reduce (fn [acc x] (conj acc (* x x))) [] [1 2 3 4 5])
; => [1 4 9 16 25]  (build vector of squares)
reduce is the most powerful (can implement map, filter, and more using reduce!).
Returning Functions (Closures)
(defn make-multiplier [factor]
  (fn [x] (* x factor)))  ; Returns a function!
(def times-10 (make-multiplier 10))
(def times-100 (make-multiplier 100))
(times-10 5)   ; => 50
(times-100 5)  ; => 500
The returned function "closes over" factor (remembers it). This is a closure.
Composition: The Heart of FP
Mathematical composition:
f(x) = x + 1
g(x) = x * 2
(g ∘ f)(x) = g(f(x)) = (x + 1) * 2
Functional programming:
(defn f [x] (+ x 1))
(defn g [x] (* x 2))
(defn g-of-f [x]
  (g (f x)))
(g-of-f 5)  ; => 12  (5+1=6, 6*2=12)
Using comp (composition function):
(def g-of-f (comp g f))
(g-of-f 5)  ; => 12
Or threading macros:
(-> 5
    f    ; 6
    g)   ; 12
Same idea as Unix pipes: Data flows through transformations.
Declarative vs Imperative
Imperative (HOW to do it)
// Sum of squares of even numbers
let sum = 0;
for (let i = 0; i < arr.length; i++) {
  if (arr[i] % 2 === 0) {
    sum += arr[i] * arr[i];
  }
}
Steps: Initialize sum, loop, check condition, update sum.
Declarative (WHAT you want)
(->> arr
     (filter even?)
     (map #(* % %))
     (reduce +))
Transformation pipeline: Filter evens → square each → sum.
Benefits of declarative:
- Reads like English ("filter even, map square, reduce add")
- No loop indices (no off-by-one errors!)
- No mutable accumulator (no sum = sum + ...)
- Easier to parallelize (map/filter can run in parallel—no shared state)
Why FP Is Winning
1. Concurrency
Mutable state + threads = race conditions:
# Two threads incrementing counter
counter = 0
def increment():
    global counter
    counter = counter + 1  # NOT ATOMIC!
# Thread 1: read 0, add 1, write 1
# Thread 2: read 0, add 1, write 1  (race!)
# Result: 1 (should be 2)
Fix: Locks (complex, slow, deadlock-prone).
FP approach: Immutable data
;; No mutation → no race conditions
(defn increment [counter]
  (+ counter 1))
;; Call from any thread—safe!
For actual shared state: Clojure's atoms (compare-and-swap, lock-free).
2. Testability
Pure functions are trivial to test:
(defn add [x y] (+ x y))
;; Test:
(assert (= (add 2 3) 5))
(assert (= (add 0 0) 0))
(assert (= (add -1 1) 0))
;; No setup, no mocking, no teardown
Impure functions:
function saveUser(user) {
  database.insert(user);  // Side effect!
  sendEmail(user.email);  // Side effect!
  logAudit(user.id);      // Side effect!
}
// Test: Mock database, email service, logger...
// Complex, fragile, slow
3. Reasoning
Pure functions are equations:
(defn total [prices]
  (reduce + prices))
;; You can reason algebraically:
(total []) = 0  (identity)
(total [a]) = a
(total [a b]) = a + b (associative, commutative)
Impure functions: Must trace through execution, track state, consider order.
FP Concepts in Other Languages
FP isn't Clojure-only. It's spreading:
JavaScript (Functional Style)
// Imperative
let doubled = [];
for (let i = 0; i < arr.length; i++) {
  doubled.push(arr[i] * 2);
}
// Functional
const doubled = arr.map(x => x * 2);
ES6+ embraced FP: map, filter, reduce, arrow functions, const/let.
Python (Functional Tools)
# map, filter, reduce
from functools import reduce
nums = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, nums))
evens = list(filter(lambda x: x % 2 == 0, nums))
total = reduce(lambda acc, x: acc + x, nums)
Or comprehensions (functional in spirit):
doubled = [x * 2 for x in nums]
evens = [x for x in nums if x % 2 == 0]
Rust (FP + Safety)
let nums = vec![1, 2, 3, 4, 5];
let doubled: Vec<_> = nums.iter()
    .map(|x| x * 2)
    .collect();
let total: i32 = nums.iter().sum();
Rust combines FP (immutability, composition) with systems programming (no GC, memory safety).
Hands-On: FP Practice
Exercise 1: Refactor to Pure
Given (impure):
let total = 0;
function addToTotal(x) {
  total += x;
  return total;
}
Refactor (pure):
function add(total, x) {
  return total + x;
}
// Call site manages state
let total = 0;
total = add(total, 5);
total = add(total, 3);
Or with reduce:
const total = [5, 3, 7].reduce(add, 0);
Exercise 2: Composition
Build a pipeline:
;; Given: list of numbers
;; Want: sum of squares of even numbers
;; Step 1: Filter evens
(filter even? [1 2 3 4 5 6])  ; => (2 4 6)
;; Step 2: Square each
(map #(* % %) [2 4 6])  ; => (4 16 36)
;; Step 3: Sum
(reduce + [4 16 36])  ; => 56
;; Compose:
(->> [1 2 3 4 5 6]
     (filter even?)
     (map #(* % %))
     (reduce +))  ; => 56
Try: Sum of cubes of odd numbers in [1..10].
Exercise 3: Higher-Order Function
Write your own map:
(defn my-map [f coll]
  (if (empty? coll)
    '()
    (cons (f (first coll))
          (my-map f (rest coll)))))
(my-map inc [1 2 3])  ; => (2 3 4)
(my-map square [1 2 3])  ; => (1 4 9)
Recursive definition:
- Empty list → empty result
- Non-empty → apply fto first, recurse on rest
This is how map is actually defined (conceptually—optimized in practice).
Common FP Patterns
1. Map-Reduce
Pattern: Transform each item (map), then combine (reduce).
;; Total price of items
(def items [{:name "Widget" :price 10}
            {:name "Gadget" :price 20}
            {:name "Gizmo" :price 15}])
(->> items
     (map :price)   ; Extract prices: (10 20 15)
     (reduce +))    ; Sum: 45
Scales to distributed computing (MapReduce, Hadoop, Spark—same pattern!).
2. Pipeline (Threading)
Pattern: Data flows through transformations.
(-> data
    parse       ; data → parsed
    validate    ; parsed → validated
    transform   ; validated → transformed
    save)       ; transformed → saved
Each step returns new data. No mutation.
Like Unix pipes, but for data structures.
3. Currying (Partial Application)
Currying: Convert f(x, y) to f(x)(y).
;; Normal function
(defn add [x y] (+ x y))
(add 10 5)  ; => 15
;; Partial application (fix first argument)
(def add10 (partial add 10))
(add10 5)  ; => 15
(add10 20) ; => 30
Useful for creating specialized functions from general ones:
(def log-error (partial log :error))
(def log-info (partial log :info))
(log-error "Something broke")  ; Same as: (log :error "Something broke")
FP and Mathematics
Lambda Calculus (Alonzo Church, 1930s)
The mathematical foundation of FP:
λx. x + 1        ; Function taking x, returning x+1
(λx. x + 1) 5    ; Apply to 5 → 6
All computable functions can be expressed in lambda calculus (Church-Turing thesis).
Modern FP languages are lambda calculus + practical features (types, I/O, performance).
Category Theory
Composition in category theory:
Objects: Types (Integer, String, User)
Morphisms: Functions (Integer → String)
Composition: f: A → B, g: B → C ⇒ g ∘ f: A → C
Laws:
- Associativity: (h ∘ g) ∘ f = h ∘ (g ∘ f)
- Identity: id ∘ f = f ∘ id = f
FP respects these laws:
;; Associativity
(comp h (comp g f)) ≡ (comp (comp h g) f) ≡ (comp h g f)
;; Identity
(comp identity f) ≡ (comp f identity) ≡ f
We'll explore this deeply in Essay 9730: Category Theory for Programmers.
FP vs OOP
The eternal debate: Functional vs Object-Oriented Programming.
OOP Approach
class User {
  private String name;
  private int age;
  
  public User(String name, int age) {
    this.name = name;
    this.age = age;
  }
  
  public void haveBirthday() {
    this.age += 1;  // Mutation!
  }
  
  public boolean isAdult() {
    return this.age >= 18;
  }
}
User alice = new User("Alice", 17);
alice.haveBirthday();  // Mutated!
OOP bundles:
- Data (fields)
- Behavior (methods)
- Identity (this object vs that object)
FP Approach
;; Just data
(def alice {:name "Alice" :age 17})
;; Functions on data
(defn have-birthday [user]
  (update user :age inc))
(defn adult? [user]
  (>= (:age user) 18))
;; Use:
(def older-alice (have-birthday alice))
(adult? older-alice)  ; => true
;; Original unchanged:
alice  ; => {:name "Alice", :age 17}
FP separates:
- Data (maps, vectors)
- Behavior (functions)
- Identity (managed explicitly if needed)
Which Is Better?
It depends.
OOP strengths:
- Encapsulation (hide implementation details)
- Polymorphism (different objects, same interface)
- Familiar (most programmers learned OOP first)
FP strengths:
- Testability (pure functions)
- Concurrency (immutability)
- Composition (small functions → complex behavior)
- Reasoning (equations, not stateful objects)
Modern synthesis: Use both!
- OOP for structure (modules, namespaces, encapsulation)
- FP for logic (pure functions, immutability, composition)
Clojure's approach: FP by default, OOP when needed (protocols, records).
Try This
Exercise 1: Refactor to Immutable
Imperative (mutation):
function updateUsers(users) {
  for (let user of users) {
    user.lastSeen = Date.now();
  }
  return users;
}
Functional (immutable):
(defn update-users [users]
  (map #(assoc % :last-seen (now)) users))
Benefits:
- Original usersunchanged (no side effect)
- Easier to test (just call with sample data)
- Parallel-safe (no mutation)
Exercise 2: Build with Higher-Order Functions
Problem: Given list of numbers, return list of those that are perfect squares.
Imperative:
result = []
for n in nums:
    if int(n ** 0.5) ** 2 == n:
        result.append(n)
Functional:
(defn perfect-square? [n]
  (let [root (Math/sqrt n)]
    (= n (* root root))))
(filter perfect-square? nums)
One line! (Plus the helper function.)
Exercise 3: Compose Complex Behavior
Problem: Process a list of users:
- Filter active users
- Extract names
- Sort alphabetically
- Take first 10
- Join with commas
Functional composition:
(->> users
     (filter :active?)
     (map :name)
     (sort)
     (take 10)
     (clojure.string/join ", "))
Five transformations, one pipeline. Clear, testable, composable.
Going Deeper
Related Essays
- 9504: What Is Clojure? - FP in practice
- 9530: Simplicity - Why FP aligns with simplicity
- 9540: Types and Sets - Mathematical foundations
- 9730: Category Theory - Mathematical structure of FP
External Resources
- "Structure and Interpretation of Computer Programs" (SICP) - Classic FP text
- Rich Hickey, "The Value of Values" talk - Why immutability matters
- "Functional Programming in JavaScript" by Luis Atencio
- "Learn You a Haskell" - Pure FP language (more extreme than Clojure)
For the Mathematically Curious
- Lambda calculus - Church's original formulation
- Curry-Howard correspondence - Programs are proofs!
- Haskell - The purest FP language (forces purity via type system)
Reflection Questions
- Can all programs be pure? (No—I/O is inherently impure. But you can isolate impurity.)
- Is immutability too expensive? (Persistent data structures make it O(log n), not O(n))
- Why isn't FP more popular? (Unfamiliar, steeper learning curve—but gaining adoption)
- When should you use mutation? (Performance hotspots, interfacing with mutable APIs—but minimize)
- Is OOP dead? (No—but FP ideas are infiltrating OOP languages)
Summary
Functional Programming:
- Treats computation as function evaluation (not sequential instructions)
- Pure functions (same input → same output, no side effects)
- Immutable data (values never change)
- Higher-order functions (functions as arguments/results)
- Composition (build complex from simple)
Key Insights:
- Purity enables testability (just call the function!)
- Immutability enables concurrency (no locks needed)
- Composition enables reuse (small functions → unlimited combinations)
- Declarative style (say what, not how) is clearer
Benefits:
- Fewer bugs (no mutation, no hidden state)
- Easier testing (pure functions, no setup)
- Better concurrency (immutable data, no races)
- Mathematical reasoning (functions are equations)
Trade-offs:
- Unfamiliar (learning curve for imperative programmers)
- Performance (sometimes mutation is faster—but optimize later)
- Purity limits (some problems need effects—IO, randomness, time)
In the Valley:
- FP is the default (Clojure, Nix expressions, Haskell-inspired thinking)
- Immutability is sacred (treat data as immutable until proven necessary to mutate)
- Composition is key (small functions, Unix-style pipelines)
- Simple over clever (clear code > terse code)
Next: We'll explore Rich Hickey's "Simple Made Easy" in depth—the philosophy underlying both Clojure and the valley itself.
Navigation:
 ← Previous: 9516 (complete stack in action) | Phase 1 Index | Next: 9530 (rich hickey simple made easy)
Bridge to Narrative: For FP storytelling, see 9949 (The Wise Elders) - Clojure as the Functional Sage!
Metadata:
- Phase: 1 (Foundations)
- Week: 2
- Prerequisites: 9504, 9510
- Concepts: Pure functions, immutability, higher-order functions, map/filter/reduce, composition, declarative vs imperative
- Next Concepts: Simplicity philosophy, decomplecting, Rich Hickey's design principles
Copyright © 2025 kae3g | Dual-licensed under Apache-2.0 / MIT
 Competitive technology in service of clarity and beauty