Lecture notes — More on Functional Programming
ver. 1.0.0
← More on Functional Programming
Where we are
In Evolving Clojure Through Macros you learned to change the language. This unit puts that aside and goes back to using it — taking one idea, the first-class function, much further than the earlier units did.
Unit 3 introduced higher-order functions and defined closures. Here both get pushed until something unexpected happens: by the end you will have built an object system in Clojure, with private state and message dispatch, from nothing but closures.
Then we ask whether you should, and the answer will be mostly no. The exercise is what makes the answer meaningful.
What you will be able to do
define-a-higher-order-function— Define what makes a function higher-order and say why it matters.collect-results-with-map— Collect the results of applying a function across a collection.reduce-and-filter-collections— Reduce a collection to a single value and filter it down to the elements you want.use-partial-application— Fix some arguments now and supply the rest later.adapt-functions-to-a-context— Adapt a general function to a specific context by fixing its varying parts.identify-free-variables— Identify a function’s free variables and explain what a closure captures.write-closures-that-carry-state— Write a function that returns a function carrying captured state.use-closures-to-delay-computation— Use a closure to delay computation and build your own control structure.build-objects-from-closures— Build message-passing objects out of closures, and say what that reveals.contrast-data-abstraction-with-objects— Contrast Clojure’s data-oriented approach with the object system you just built.
What we will cover
- Higher-order functions — deriving
map,reduceandfilterrather than being handed them. - Lazy sequences — why
do-to-allneedslazy-seqto survive a large input. - Partial application — fixing arguments now and supplying the rest later.
- Lexical closures — free variables, captured state, and information hiding.
- Message-passing objects — closures that dispatch on a keyword.
- Data abstraction versus objects — why Clojure prefers plain data.
Functions all the way down
Two halves, and the second is where it gets interesting.
Higher-order functions — collecting, reducing, filtering, partial application, adapting.
Closures — free variables, delayed computation, and closures that carry state.
Learning outcomes
- define-a-higher-order-function: Define what makes a function higher-order and say why it matters.
- identify-free-variables: Identify a function’s free variables and explain what a closure captures.
Higher-order functions in practice
The core functions were handed to you in unit 2. Here we derive them, which is a better way to understand what they are for.
Collecting results
Start with squaring every element:
(defn square [x]
(* x x))
(defn square-all [numbers]
(if (empty? numbers)
()
(cons (square (first numbers))
(square-all (rest numbers)))))
(square-all [1 2 3 4 5 6])
;=> (1 4 9 16 25 36)Now cubing:
(defn cube [x]
(* x x x))
(defn cube-all [numbers]
(if (empty? numbers)
()
(cons (cube (first numbers))
(cube-all (rest numbers)))))
(cube-all [1 2 3 4 5 6])
;=> (1 8 27 64 125 216)Put them side by side. They are identical except for one function call. Pull that out as a parameter:
(defn do-to-all [f numbers]
(if (empty? numbers)
()
(cons (f (first numbers))
(do-to-all f (rest numbers)))))
(do-to-all square [1 2 3 4 5 6])
;=> (1 4 9 16 25 36)
(do-to-all cube [1 2 3 4 5 6])
;=> (1 8 27 64 125 216)You have written map.
Where it breaks, and laziness
(do-to-all square (range 11000))
StackOverflowError clojure.lang.Numbers$LongOps.multiply (Numbers.java:459)The recursion is not in tail position — cons wraps it — so recur cannot help. The fix is laziness:
(defn do-to-all [f numbers]
(lazy-seq
(if (empty? numbers)
()
(cons (f (first numbers))
(do-to-all f (rest numbers))))))
(take 10 (drop 10000 (do-to-all square (range 11000))))
;=> (100000000 100020001 100040004 100060009 100080016 ...)lazy-seq means the recursive call is not made until the next element is asked for, so the stack never deepens. Clojure’s real map is lazy for exactly this reason.
The work has not happened when map returns. That is usually invisible and occasionally decisive — a lazy sequence over side-effecting work is a trap, which is why doseq and doall exist. Unit 3’s dynamic-var example was the same problem wearing a different hat.
Reducing
Summing:
(defn total-of [numbers]
(loop [nums numbers sum 0]
(if (empty? nums)
sum
(recur (rest nums) (+ sum (first nums))))))
(total-of [5 7 9 3 4 1 2 8])
;=> 39Finding the largest:
(defn larger-of [x y]
(if (> x y) x y))
(defn largest-of [numbers]
(loop [l numbers candidate (first numbers)]
(if (empty? l)
candidate
(recur (rest l) (larger-of candidate (first l))))))
(largest-of [5 7 9 3 4 1 2 8])
;=> 9
(largest-of [])
;=> nilSame shape again — walk the collection carrying an accumulator, differing only in how they combine. Abstract it:
(defn compute-across [func elements value]
(if (empty? elements)
value
(recur func (rest elements) (func value (first elements)))))
(defn total-of [numbers]
(compute-across + numbers 0))
(defn largest-of [numbers]
(compute-across larger-of numbers (first numbers)))Both originals are now one-liners. You have written reduce.
And it does more than arithmetic:
(defn all-greater-than [threshold numbers]
(compute-across #(if (> %2 threshold) (conj %1 %2) %1) numbers []))
(all-greater-than 5 [5 7 9 3 4 1 2 8])
;=> [7 9 8]The accumulator is a vector rather than a number. With Clojure’s reduce — note the argument order differs:
(defn all-greater-than [threshold numbers]
(reduce #(if (> %2 threshold) (conj %1 %2) %1) [] numbers))Filtering
(defn all-lesser-than [threshold numbers]
(compute-across #(if (< %2 threshold) (conj %1 %2) %1) numbers []))
(all-lesser-than 5 [5 7 9 3 4 1 2 8])
;=> [3 4 1 2]Compare with all-greater-than: identical but for the comparison. Extract it:
(defn select-if [pred elements]
(compute-across #(if (pred %2) (conj %1 %2) %1) elements []))
(select-if odd? [5 7 9 3 4 1 2 8])
;=> [5 7 9 3 1]
(defn all-lesser-than [threshold numbers]
(select-if #(< % threshold) numbers))You have written filter:
(filter odd? [5 7 9 3 4 1 2 8])
;=> (5 7 9 3 1)Every one of these came from noticing that two functions differed in one place, and making that place a parameter. That move is the whole unit in a sentence.
Learning outcomes
- define-a-higher-order-function: Define what makes a function higher-order and say why it matters.
- collect-results-with-map: Collect the results of applying a function across a collection.
- reduce-and-filter-collections: Reduce a collection to a single value and filter it down to the elements you want.
Concepts
- higher-order-functions: derives map, reduce and filter by abstracting the difference between near-identical functions
- lazy-sequences: shows lazy-seq rescuing a non-tail recursion over a large input
Partial application
Adapting a function to a context
A price calculation with tax:
(defn price-with-tax [tax-rate amount]
(-> (/ tax-rate 100)
(+ 1)
(* amount)))
(price-with-tax 9.5M 100)
;=> 109.500MNow you need California prices:
(defn with-california-taxes [prices]
(map #(price-with-tax 9.25M %) prices))
(def prices [100 200 300 400 500])
(with-california-taxes prices)
;=> (109.2500M 218.5000M 327.7500M 437.0000M 546.2500M)Then New York, then everywhere else. The naive route is one function per state:
(defn price-with-ca-tax [price]
(price-with-tax 9.25M price))
(defn price-with-ny-tax [price]
(price-with-tax 8.0M price))Duplication, and a new function for every jurisdiction. Write a factory instead:
(defn price-calculator-for-tax [state-tax]
(fn [price]
(price-with-tax state-tax price)))
(def price-with-ca-tax (price-calculator-for-tax 9.25M))
(def price-with-ny-tax (price-calculator-for-tax 8.0M))Each result is an ordinary one-argument function. Callers never know about tax rates.

Note what is really happening: the returned function is a closure over state-tax. This section looks like it is about partial application, and it is the first place captured state does something useful.
Generalising it
Take a function of five arguments:
(defn of-n-args [a b c d e]
(str a b c d e))Fixing the first three by hand:
(defn of-k-args [d e]
(of-n-args 1 2 3 d e))
(of-k-args \a \b)
;=> "123ab"Generalise it, and the mechanism is again a closure:
(defn partially-applied [of-n-args & n-minus-k-args]
(fn [& k-args]
(apply of-n-args (concat n-minus-k-args k-args))))
(def of-2-args (partially-applied of-n-args \a \b \c))
(def of-3-args (partially-applied of-n-args \a \b))
(of-2-args 4 5)
;=> "abc45"
(of-3-args 3 4 5)
;=> "ab345"Clojure has this built in as partial, used identically:
(def of-2-args (partial of-n-args \a \b \c))
(def of-3-args (partial of-n-args \a \b))Having written it yourself, partial reads as a convenience rather than magic.
Argument order is a design decision
(defn select-into-if [container pred elements]
(compute-across #(if (pred %2) (conj %1 %2) %1) elements container))
(def numbers [4 9 5 7 6 3 8])
(select-into-if [] #(< % 7) numbers)
;=> [4 5 6 3]
(select-into-if () #(< % 7) numbers)
;=> (3 6 5 4)Note the second: conjoining onto a list adds at the front, so the result is reversed. With the container first, both are partially applicable:
(def select-up (partial select-into-if []))
(def select-down (partial select-into-if ()))You can only fix arguments from the left. So a function taking its configuration first and its data last partially-applies well, and one taking them the other way round does not. This is why (map f coll) puts the function first, and it is a real consideration when designing your own.
Learning outcomes
- use-partial-application: Fix some arguments now and supply the rest later.
- adapt-functions-to-a-context: Adapt a general function to a specific context by fixing its varying parts.
Concepts
- partial-application: builds partial application by hand and then with partial
- lexical-closures: shows the closure underneath every adapted function
Closures and free variables
(defn adder [num1 num2]
(let [x (+ num1 num2)]
(fn [y]
(+ x y))))
(def add-5 (adder 2 3))
(add-5 10)
;=> 15The inner function uses x, which is neither its parameter nor bound in its body. x is a free variable, and the inner function closes over it — which is where the name comes from. A function with free variables is “open”: you cannot say what it computes without knowing what those names refer to. Capturing them closes it.
Three things to notice:
adderreturned long ago, and itsxis still thereThe captured binding outlives the form that created it.
(adder 2 3)and(adder 10 20)produce independent functionsEach closure gets its own capture, so they cannot interfere.
add-5takes one argument, not threeIt was configured at creation rather than parameterised at every call.
Learning outcomes
- identify-free-variables: Identify a function’s free variables and explain what a closure captures.
- write-closures-that-carry-state: Write a function that returns a function carrying captured state.
Concepts
- lexical-closures: defines closures through free variables and shows captured state persisting
Delayed computation
Start with something that fails:
(let [x 1
y 0]
(/ x y))
ArithmeticException Divide by zero clojure.lang.Numbers.divide (Numbers.java:156)Wrap it:
(let [x 1
y 0]
(try
(/ x y)
(catch Exception e (println (.getMessage e)))))
Divide by zero
;=> nilThat pattern repeats often enough to be worth extracting — into a higher-order control structure:
(defn try-catch [the-try the-catch]
(try
(the-try)
(catch Exception e (the-catch e))))and used:
(let [x 1
y 0]
(try-catch #(/ x y)
#(println (.getMessage %))))Wrapping a computation in a function of no arguments postpones it. Nothing happens until someone calls it. That single move lets a function receive unevaluated work — otherwise the exclusive privilege of macros. Exception handling has become a value you can pass around and reuse.
Both delay evaluation, differently:
- a closure delays it by wrapping in a function. The caller writes the wrapper, so it is visible at the call site, and the result is an ordinary value.
- a macro delays it by rewriting code. The caller writes ordinary-looking code, but the construct is not a value and cannot be composed.
The trade is syntax versus composability. Unit 7’s unless had to be a macro, because requiring callers to wrap bodies in #() would have been intolerable — and easy to forget. Here, where the wrapper is a deliberate reusable abstraction, the extra #() is a fair price for getting a value back.
Knowing both, and knowing which trade you are making, is the skill.
Learning outcomes
- use-closures-to-delay-computation: Use a closure to delay computation and build your own control structure.
- write-closures-that-carry-state: Write a function that returns a function carrying captured state.
Concepts
- lexical-closures: uses a closure to postpone evaluation and build a control structure
Closures as objects
Now the surprise.
(defn new-user [login password email]
(fn [a]
(case a
:login login
:password password
:email email
nil)))
(def arjun (new-user "arjun" "secret" "arjun@zololabs.com"))
(arjun :login) ;=> "arjun"
(arjun :password) ;=> "secret"
(arjun :email) ;=> "arjun@zololabs.com"
(arjun :name) ;=> nilA closure capturing three values, dispatching on a message. Nothing new mechanically — and it behaves exactly like an object.
Information hiding
Change one line and the password stops being readable:
(defn new-user [login password email]
(fn [a]
(case a
:login login
:email email
:password-hash (hash password)
nil)))
(def arjun (new-user "arjun" "secret" "arjun@zololabs.com"))
(arjun :password)
;=> nil
(arjun :password-hash)
;=> 1614358358The inner function can see password; callers cannot. Genuinely private state, from a closure.
Adding behaviour
(defn new-user [login password email]
(fn [a & args]
(case a
:login login
:email email
:authenticate (= password (first args)))))
(def adi (new-user "adi" "secret" "adi@currylogic.com"))
(adi :authenticate "blah")
;=> false
(adi :authenticate "secret")
;=> trueWhich gives the general form:
(object message-name & arguments)
Objects in OOP are usually defined as entities that have state, behavior, and equality.
We now have state (login, password, email) and behaviour (:authenticate).
Is it data or a function?
Although
arjunis a function, semantically it looks and behaves like data.
It behaves like a hash map you can query — except that you decide which keys are public. Because it is a closure, the free variables passed to new-user are captured and stay alive as long as the closure does.
The object system
The chapter generalises this into a small class system with defclass, inheritance, this, and method definitions:
(defn new-class [class-name]
(fn [command & args]
(case command
:name (name class-name))))
(defmacro defclass [class-name]
`(def ~class-name (new-class '~class-name)))(defclass Person)
(Person :name)
;=> "Person"Built up with methods, state and inheritance, the whole thing runs to a little over 50 lines.
The key to this implementation is the lexical closure. It’s up to you to take a stand on the old debate: Are objects a poor man’s closures, or is it the other way around?
Fewer than half the lines manipulate functions. The rest — defclass and its supporting functions — exist to make the syntax look a certain way.
The semantics would not change under a different syntax. The syntax is not what makes the code useful; the underlying mechanisms are. Though a nice syntax does help: it is convenient, and because the code is data you can write checkers that analyse it and give meaningful errors.
Learning outcomes
- build-objects-from-closures: Build message-passing objects out of closures, and say what that reveals.
- write-closures-that-carry-state: Write a function that returns a function carrying captured state.
Concepts
- message-passing-objects: builds objects with private state and message dispatch from closures
- lexical-closures: shows capture providing genuine information hiding
What you can now abstract
You built an object system. Should you use it?
in most cases, such artificial constructs are unnecessary in languages such as Clojure.
Two reasons.
Abstraction. You do not need objects for data abstraction. Clojure’s core data structures are the real alternative — each implementation of the sequence abstraction is a candidate for representing data. They are immutable and therefore thread safe, so there is no need for procedural abstractions wrapping their mutation. And when you need a new abstraction that existing types should participate in, that is what protocols are for.
Alan Perlis’s epigram, which is the more memorable argument:
it’s better to have 100 functions that operate on a single data structure instead of 10 functions that operate on 10 data structures.
A common data structure allows more code reuse, because code that works on sequences works no matter what specific data it contains — as the entire sequence library demonstrates.
There is also a practical cost:
creating an object system like the one created in the second half raises a barrier of inoperability with other libraries that don’t know about it.
A map is transparent, printable, comparable and serialisable, and any function can consume it. A closure’s captured state is opaque, and only answers what it chooses to.
So why build one? Because the exercise establishes the equivalence, and the equivalence is what makes the choice informed. Deciding to use maps because objects are unavailable is not a decision. Deciding to use maps having built objects and seen what they cost is.
Learning outcomes
- contrast-data-abstraction-with-objects: Contrast Clojure’s data-oriented approach with the object system you just built.
- define-a-higher-order-function: Define what makes a function higher-order and say why it matters.
- collect-results-with-map: Collect the results of applying a function across a collection.
- reduce-and-filter-collections: Reduce a collection to a single value and filter it down to the elements you want.
- use-partial-application: Fix some arguments now and supply the rest later.
- adapt-functions-to-a-context: Adapt a general function to a specific context by fixing its varying parts.
- identify-free-variables: Identify a function’s free variables and explain what a closure captures.
- write-closures-that-carry-state: Write a function that returns a function carrying captured state.
- use-closures-to-delay-computation: Use a closure to delay computation and build your own control structure.
- build-objects-from-closures: Build message-passing objects out of closures, and say what that reveals.
Concepts
- data-abstraction-vs-objects: argues for plain data over closure-based objects, via Perlis’s epigram
- higher-order-functions: collects the derived core functions
- lazy-sequences: collects laziness as the property that makes them work at scale
Conclusion
Every core sequence function comes from abstracting one difference.
square-allandcube-alldiffer in a function — that ismap.total-ofandlargest-ofdiffer in a combining function — that isreduce.all-greater-thanandall-lesser-thandiffer in a predicate — that isfilter.Laziness is what lets those functions survive a large input.
do-to-alloverflows the stack at 11,000 elements becauseconsputs the recursion out of tail position.lazy-seqdefers each step until it is asked for.Partial application is a closure, and argument order decides whether it is possible.
You can only fix arguments from the left. Put the parts that vary least first, which is why
(map f coll)is ordered as it is.A closure outlives the scope that created it, and each gets its own capture.
adderreturns and itsxpersists. Two closures from one factory never interfere, which is what makes them usable as independent things.Closures give private state and message dispatch — which is to say, objects.
new-usercan seepasswordand callers can only get its hash. Add:authenticateand you have behaviour. The object system that follows is a little over 50 lines, and half of it is syntax.Knowing you can build objects is what lets you choose not to.
Clojure prefers plain data: transparent, printable, comparable, and consumable by any function. A hundred functions on one data structure beats ten on ten.
Where next
The next unit, More Macros and DSLs, returns to the macro thread with the same shape of ambition: not extending the language a construct at a time, but building a small language of your own on top of it — anaphoric macros, compile-time computation, macros that write macros, and a domain-specific language.