Abstract: Exploring Closures via Metaphor
This writeup explores the concept of closures in Clojure through explanatory analogies and practical examples. We present multiple mental models to help developers understand and reason about closures, with special attention to closures containing mutable state through atoms. By providing both conceptual frameworks and concrete code samples, we aim to demystify this fundamental functional programming concept.
1. Introduction
Closures represent one of the most powerful yet often misunderstood concepts in functional programming. In Clojure, closures enable a wide range of programming patterns and are fundamental to how the language operates. This paper presents several analogies to help developers better conceptualize and work with closures effectively.
2. Understanding Closures Through Analogies
2.1 The Backpack Analogy
A closure is like a backpack that a function carries, containing all the variables it needs from where it was created.
(defn prepare-for-journey [destination]
(let [supplies (str "Map of " destination)
equipment "Compass"]
(fn [terrain]
(println "Navigating" terrain "in" destination
"using" supplies "and" equipment))))
;; The traveler packs their backpack
(def mountain-journey (prepare-for-journey "Alps"))
;; Later, they use what's in their backpack
(mountain-journey "steep ridge")
;; => "Navigating steep ridge in Alps using Map of Alps and Compass"
The inner function carries its "backpack" containing destination
, supplies
, and equipment
wherever it goes, even after leaving the original function's scope.
2.2 The Takeout Container Analogy
A closure packages up variables like a takeout container packages food for consumption elsewhere.
(defn make-counter [start-value]
;; Package up the start-value and counter in a "takeout container"
(let [count-atom (atom start-value)]
;; This function takes the container with it
(fn []
(swap! count-atom inc))))
;; Order three different takeout meals
(def counter-from-10 (make-counter 10))
(def counter-from-100 (make-counter 100))
;; Each container has its own food inside
(counter-from-10) ;; => 11
(counter-from-10) ;; => 12
(counter-from-100) ;; => 101
Each counter function carries its own "takeout container" with its private state, completely separate from other instances.
2.3 The Passport with Visa Analogy
A closure is like a passport that gives a function access to variables from its country of origin.
(defn create-translator [base-language]
(let [credentials (str "Certified " base-language " translator")
translation-memory {"hello" {"Spanish" "hola"
"French" "bonjour"}}]
(fn [word target-language]
(str credentials " says: "
word " in " target-language " is "
(get-in translation-memory [word target-language])))))
;; The function gets its passport with visa
(def english-translator (create-translator "English"))
;; It can access its home country's resources from anywhere
(english-translator "hello" "Spanish")
;; => "Certified English translator says: hello in Spanish is hola"
2.4 The Sealed Envelope Analogy
A closure is like a sealed envelope containing information that can only be opened by the specific function it was created for.
(defn create-bank-account [initial-balance]
;; Create a sealed envelope with private information
(let [balance (atom initial-balance)
account-number (str (rand-int 10000))]
;; Return a map of functions that can access the sealed envelope
{:deposit (fn [amount]
(swap! balance + amount)
(str "Deposited " amount " to account " account-number
". New balance: " @balance))
:withdraw (fn [amount]
(if (>= @balance amount)
(do
(swap! balance - amount)
(str "Withdrew " amount " from account " account-number
". New balance: " @balance))
(str "Insufficient funds in account " account-number)))}))
;; Create an account with its sealed envelope
(def account (create-bank-account 1000))
;; Only these functions can access the private information
((account :deposit) 500) ;; => "Deposited 500 to account 1234. New balance: 1500"
((account :withdraw) 200) ;; => "Withdrew 200 from account 1234. New balance: 1300"
3. Closures with Mutable State: Advanced Analogies
Closures that contain atoms deserve special attention as they introduce mutable state in a predominantly immutable language.
3.1 The Spring-loaded Trap Analogy
Closures containing atoms are like spring-loaded traps that can be triggered multiple times, each time potentially changing their internal state.
(defn create-toggle []
(let [state (atom :off)]
(fn []
(swap! state #(if (= % :off) :on :off))
@state)))
(def light-switch (create-toggle))
(light-switch) ;; => :on
(light-switch) ;; => :off
(light-switch) ;; => :on
Like a trap that can be reset and sprung again, the toggle function changes its internal state each time it's invoked.
3.2 The Climate-Controlled Terrarium Analogy
Closures with atoms are like climate-controlled terrariums - sealed environments where conditions can be monitored and modified, but only through specific interfaces.
(defn create-temperature-monitor [initial-temp]
(let [temperature (atom initial-temp)
history (atom [])]
{:current (fn [] @temperature)
:record (fn [new-temp]
(swap! temperature (constantly new-temp))
(swap! history conj {:time (java.util.Date.) :temp new-temp})
@temperature)
:history (fn [] @history)
:average (fn []
(if (empty? @history)
@temperature
(/ (reduce + (map :temp @history)) (count @history))))}))
(def greenhouse (create-temperature-monitor 22.5))
((greenhouse :record) 23.8) ;; => 23.8
((greenhouse :record) 24.2) ;; => 24.2
((greenhouse :current)) ;; => 24.2
((greenhouse :average)) ;; => 24.0
The terrarium maintains its own internal environment that can be modified and observed only through specific functions.
3.3 The Ship in a Bottle Analogy
Closures with atoms are like ships in bottles - carefully constructed structures with moving parts, sealed inside a container that allows visibility but controlled access.
(defn create-inventory-system []
(let [inventory (atom {})
transaction-log (atom [])]
{:add (fn [item quantity]
(swap! inventory update item (fnil + 0) quantity)
(swap! transaction-log conj {:type :add :item item :quantity quantity :timestamp (System/currentTimeMillis)})
(get @inventory item))
:remove (fn [item quantity]
(if (>= (get @inventory item 0) quantity)
(do
(swap! inventory update item - quantity)
(swap! transaction-log conj {:type :remove :item item :quantity quantity :timestamp (System/currentTimeMillis)})
true)
false))
:status (fn [] @inventory)
:log (fn [] @transaction-log)}))
(def warehouse (create-inventory-system))
((warehouse :add) :books 100) ;; => 100
((warehouse :add) :pens 50) ;; => 50
((warehouse :remove) :books 25) ;; => true
((warehouse :status)) ;; => {:books 75, :pens 50}
Just as a ship in a bottle contains complex, interacting parts that are manipulated through a narrow opening, this inventory system maintains its internal state that can only be modified through specific operations.
4. Practical Applications of Closures in Clojure
4.1 Memoization
(defn memoize [f]
(let [cache (atom {})]
(fn [& args]
(if-let [e (find @cache args)]
(val e)
(let [ret (apply f args)]
(swap! cache assoc args ret)
ret)))))
(def slow-fibonacci
(memoize
(fn [n]
(if (<= n 1)
n
(+ (slow-fibonacci (- n 1))
(slow-fibonacci (- n 2)))))))
(time (slow-fibonacci 40)) ;; Fast after first computation
4.2 Generators
(defn make-generator [initial-seq]
(let [s (atom (seq initial-seq))]
(fn []
(when-let [xs @s]
(let [x (first xs)]
(swap! s next)
x)))))
(def gen (make-generator [1 2 3 4 5]))
(gen) ;; => 1
(gen) ;; => 2
(gen) ;; => 3
4.3 Simple State Management
(defn make-counter []
(let [count-atom (atom 0)]
{:increment (fn [] (swap! count-atom inc))
:decrement (fn [] (swap! count-atom dec))
:reset (fn [] (reset! count-atom 0))
:current (fn [] @count-atom)}))
(def counter (make-counter))
((counter :increment)) ;; => 1
((counter :increment)) ;; => 2
((counter :decrement)) ;; => 1
((counter :reset)) ;; => 0
5. Conclusion
Closures are a fundamental concept in Clojure and functional programming at large. By understanding them through appropriate analogies, developers can better reason about their behavior and use them effectively in their code. The analogies presented in this paper provide multiple perspectives on how closures work, particularly when they involve mutable state through atoms.
As we've seen, closures enable powerful programming patterns while maintaining clean, modular code. They form the backbone of many Clojure idioms and libraries, making them an essential concept for any Clojure developer to master.
References
Fogus, M., & Houser, C. (2011). The Joy of Clojure. Manning Publications.
Halloway, S. (2012). Programming Clojure. Pragmatic Bookshelf.
Emerick, C., Carper, B., & Grand, C. (2012). Clojure Programming. O'Reilly Media.