Fred Brooks once described elegant design as few ideas combined powerfully. Clojure is a language that lives by that idea. Understanding Clojure improved my understanding of language choices in general. Here I’ll highlight some of my learnings.

Macros

My number one take-away is a better understanding of macros, and in turn symbolic programming.

I previously pondered, why C#, Java, and other languages I knew didn’t have compile-time meta-programming.

Turns out, many have considered this problem before. The core category of compile-time meta-programming is known as macros.

I had known about macros, but my perception was limited to text-substitution macros. Those are only the tip of the iceberg. Procedural and syntactic macros offer much more power.

Clojure is a dialect of Lisp, the language that pioneered symbolic programming. Symbolic programming treats the program itself like data. It takes meta-programming to the extreme and allows our program to know as much about itself as we do, both at compile or runtime.

Symbolic programming extends the programming language itself and addresses a category of problems I’ve long wrestled with in C#. Problems like generating proxies, decorators, or api clients.

I also recommend Rust for exploring macros. Rust leverages macros to shape high-level language conveniences without compromising safety or security.

Set Semantics + Spec

My next lesson from Clojure expanded my concept of type systems.

Clojure idiomatically uses “set semantics” for compound data types. Instead of classes or records, everything is a dictionary using keywords to differentiate properties. Keywords are a primitive type that represent a unique name like :address.

;; a dictionary of data
{
    :name "Anders"
    :phone "555-555-5555"
    :email "example@mail.com"
}

Set semantics define a predictable structural typing paradigm. Consumers don’t care about the type, they only care that it has the right data.

However, set semantics alone leaves our typing completely open. It forces contracts between components to be implicit.

This is where Clojure.spec saves the day. Spec adds constraints onto keywords and functions. It can provide design-time feedback on component expectations without having to look at code or limit the non-essential properties of the type.

;; phone must match regex
(s/def :phone #(re-matches #"[\d{3}-\d{3}-\d{4}" %))
;; email must match regex
(s/def :email #(re-matches #"\w+@\w+\.[a-zA-Z]+" %))
;; name must be a string between 0 and 100 characters
(s/def :name (s/and 
    string?
    #(< 0 (count %))
    #(< (count %) 100)
))
(s/def :person (s/keys :req [
    :name
    :phone
    :email
]))

This is true design-by-contract and also follows the Open-Closed principle. It allows each component to guarantee it’s expectations clearly without restricting callers to bespoke types.

These ideas are flexible enough to also power the edn data format and Datomic database system.

I recommend Maybe Not by Rich Hickey for more exploration.

Transactions

My final lesson was about transactions.

I’ve always seen transactions as an infrastructure concept or an implicit design pattern. I’d never thought about them as a possible language feature.

However, Clojure bakes transactions right into the language for handling mutable data.

This makes sense. Clojure manages immutable data transforms under the hood as a series of changes to an original data structure. In essence, clojure’s data structures are a transaction log.

In fact, Datomic leverages the same idea to turn Clojure data structures into a transactional database.

This approach makes Clojure and datomic a natural fit for Event Sourcing or cases that benefit from a history of changes and not just the latest state.

Summary

Clojure is a lovingly crafted language. It is full of high-concepts that pushed my perception of programming. Macros, set semantics, and transactions were merely the most impactful for me. Clojure is worth a look for learning experience, even if doesn’t end up in your daily toolbox.