header image

Exploring the Constraints of Clojure

Published on
760 words4 min read

Evolution of Constraints

As programming languages evolved, constraints were introduced. Bits were shuttled around manually until Assembly came and replaced shifts with statements. Java removed the GOTO keyword and enforced first-class control flows and main() execution. This continues to date considering the growing popularity of TypeScript, a type-constraint over JavaScript, and even Go, which disallows pointer arithmetic.

It is a strange thing to admit, but programming languages evolve primarily through the addition of constraints. Programming languages strive to do more by offering less. Not only does this apply to language design but also programming paradigms. Functional programming, in contrast to the decidedly more mainstream Object-oriented programming, advocates far more constraints like purity, referential transparency, and composition.

Though I don't hold strong opinions, I do believe there is a lot to learn and take away from each language, especially languages written in a style far apart from languages you are already proficient in. This was my rationale for exploring Clojure and its JavaScript-counterpart ClojureScript. Clojure is unique as it's a functional language that is a modern Lisp with a focus on concurrency.

Clojure

Per its Rationale, Clojure is

A Lisp
for Functional Programming
symbiotic with an established Platform
designed for Concurrency

Lisp

Clojure, the language, is comprised entirely of lists called S-expressions nested in a tree-like structure:

(+ (- 6 1) 2) ;; (symbol (symbol x y) z)

This is a unique constraint on syntax directly leading to many wonderful macro and metaprogramming possibilities. This also means that Clojure can interpret and construct itself through a runtime reader. In fact, the Clojure language is only a superset of its own data-exchange format EDN. If JavaScript was like this, we would write JavaScript largely as JSON. But, this isn't the case as JavaScript isn't homoiconic like Clojure is.

Platform

Clojure is a hosted language on the JVM. This is in the tradition of Lisp but was done primarily to gain leverage from the well-established Java ecosystem. This also means that primitives in Clojure are from the host platform:

(instance? java.lang.String "Foo") ; true

In an arrangement like this, the language becomes more of an interface while the host becomes more of an implementation detail. This is clear when writing ClojureScript as most ClojureScript code is also valid Clojure code.

(println "Foo") ; this can run on both the JVM and a web browser

Concurrency

Design for concurrency is done primarily through Clojure's unique features of immutability and persistence.

(def my-vector [1 2 3 4])
(def my-other-vector (conj my-vector 5))

(= '[1 2 3 4] my-vector) ; true
(= '[1 2 3 4 5] my-other-vector) ; true
(= my-vector my-other-vector) ; false

conj-ing (appending) the integer 5 does two fundamental things:

  1. It creates a new vector without changing the previous value (immutability) by copying values.

  2. It copies values performantly by encoding all shared and referenced values within tries.

This can be demostrated by doing an equality check and butlast1:

(= (butlast my-other-vector) my-vector) ; true

This check is strictly on values (no reference/memory checks). Yet, my-other-vector and my-vector share the value [1 2 3 4] internally, leading to efficient copy operations and memory usage. Moreover, this all but eliminates race conditions and the need for locks in multi-threaded environments as every function invocation has stable access to all previous values.

Futher Reading

I can't recommend Micahel Fogus's Joy of Clojure enough. Not only does Fogus walk through the design and syntax of Clojure but also its idioms and rationales, answering many "whys" of Clojure that are difficult to convey.


  1. butlast returns all but the last item in a collection.