More on Functional Programming

Clojure

2026-08-20 10:45

Where we are

Back from macros

The previous unit changed the language.

This one goes back to using it —

and takes one idea much further than before.

Where this ends

By the end you will have built an object system in Clojure.

Private state. Message dispatch.

From nothing but closures.

Then we ask whether you should.

Higher-order functions in practice

Square everything

(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)

Cube everything

(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)

Identical except for one function call.

Abstract it

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

Then it breaks

(do-to-all square (range 11000))
StackOverflowError

cons wraps the recursion, so it is not in tail position.

recur cannot help.

Laziness fixes it

(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 ...)

The recursive call waits until the next element is asked for.

Reducing

(defn total-of [numbers]
  (loop [nums numbers sum 0]
    (if (empty? nums)
      sum
      (recur (rest nums) (+ sum (first nums))))))

(defn largest-of [numbers]
  (loop [l numbers candidate (first numbers)]
    (if (empty? l)
      candidate
      (recur (rest l) (larger-of candidate (first l))))))

Same shape — differing only in how they combine.

compute-across

(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)))

You have written reduce.

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, not a number.

Filtering

(defn all-lesser-than [threshold numbers]
  (compute-across #(if (< %2 threshold) (conj %1 %2) %1) numbers []))

Identical to all-greater-than 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]

You have written filter.

The whole unit in one sentence

Every one came from noticing that two functions differed in one place

and making that place a parameter.

Partial application

The situation

(defn price-with-tax [tax-rate amount]
  (-> (/ tax-rate 100)
      (+ 1)
      (* amount)))

(price-with-tax 9.5M 100)
;=> 109.500M

Now you need California. Then New York. Then everywhere.

The naive route

(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 per jurisdiction.

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.

What is really happening

Adapting a general function by fixing its varying argument.

The returned function is a closure over state-tax.

Generalising

(defn of-n-args [a b c d e]
  (str a b c d e))

(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))
(of-2-args 4 5)
;=> "abc45"

Clojure has this as partial, used identically.

Argument order is a design decision

(select-into-if [] #(< % 7) numbers)   ;=> [4 5 6 3]
(select-into-if () #(< % 7) numbers)   ;=> (3 6 5 4)

With the container first, both partially apply:

(def select-up   (partial select-into-if []))
(def select-down (partial select-into-if ()))

The rule

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.

Closures

Free variables

(defn adder [num1 num2]
  (let [x (+ num1 num2)]
    (fn [y]
      (+ x y))))

(def add-5 (adder 2 3))
(add-5 10)
;=> 15

x is neither a parameter nor bound in the body. It is free.

Why “closure”

A function with free variables is open

you cannot say what it computes without knowing those names.

Capturing them closes it.

Three things to notice

  • adder returned long ago, and x is still there
  • (adder 2 3) and (adder 10 20) are independent
  • add-5 takes one argument, not three

Configured at creation, not parameterised at every call.

Delayed computation

Something that fails

(let [x 1
      y 0]
  (/ x y))
ArithmeticException Divide by zero
(let [x 1
      y 0]
  (try
    (/ x y)
    (catch Exception e (println (.getMessage e)))))
Divide by zero
;=> nil

Extract the pattern

(defn try-catch [the-try the-catch]
  (try
    (the-try)
    (catch Exception e (the-catch e))))
(let [x 1
      y 0]
  (try-catch #(/ x y)
             #(println (.getMessage %))))

What just happened

Wrapping in a function of no arguments postpones it.

A function can now receive unevaluated work

otherwise the exclusive privilege of macros.

Exception handling has become a value.

Closure or macro?

Closure — the caller writes the wrapper. Visible at the call site. Result is a value.

Macro — rewrites code. Caller writes ordinary code. Not a value.

The trade is syntax versus composability.

Which to choose

unless had to be a macro — requiring #() everywhere would be intolerable,

and easy to forget.

Here, where the wrapper is a deliberate abstraction,

the extra #() buys you a value back.

Closures as objects

A closure that dispatches

(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 :email)     ;=> "arjun@zololabs.com"
(arjun :name)      ;=> nil

Information hiding

(defn new-user [login password email]
  (fn [a]
    (case a
      :login login
      :email email
      :password-hash (hash password)
      nil)))

(arjun :password)       ;=> nil
(arjun :password-hash)  ;=> 1614358358

The inner function can see password. Callers cannot.

Adding behaviour

(defn new-user [login password email]
  (fn [a & args]
    (case a
      :login login
      :email email
      :authenticate (= password (first args)))))

(adi :authenticate "blah")    ;=> false
(adi :authenticate "secret")  ;=> true
(object message-name & arguments)

Data or function?

Although arjun is a function, semantically it looks and behaves like data.

State ✓ · Behaviour ✓

The object system

(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: a little over 50 lines.

The old debate

Are objects a poor man’s closures, or is it the other way around?

Note how the 50 lines divide

Fewer than half manipulate functions.

The rest exist to make the syntax look a certain way.

The semantics would not change under a different syntax.

What you can now abstract

Should you use it?

in most cases, such artificial constructs are unnecessary in languages such as Clojure

Reason one: abstraction

You do not need objects for data abstraction.

Clojure’s core structures are immutable and thread safe —

no need for procedural abstractions wrapping their mutation.

Reason two: Perlis

it’s better to have 100 functions that operate on a single data structure instead of 10 functions that operate on 10 data structures

And a practical cost

creating an object system … raises a barrier of inoperability with other libraries that don’t know about it

A map is transparent, printable, comparable, serialisable.

A closure’s state is opaque.

So why build one?

Because the exercise establishes the equivalence.

Deciding to use maps because objects are unavailable is not a decision.

Deciding to use maps having built objects is.

Summary

The six things to carry away

  • Every core sequence function comes from abstracting one difference.
  • Laziness is what lets them survive a large input.
  • Partial application is a closure; argument order decides if it is possible.
  • A closure outlives its scope, and each gets its own capture.
  • Closures give private state and message dispatch — which is to say, objects.
  • Knowing you can build objects is what lets you choose not to.

Where next

More Macros and DSLs returns to the macro thread —

with the same shape of ambition.

Anaphoric macros · compile-time computation

macros that write macros · a domain-specific language