I like Clojure

Programming language discourse is silly. It gives the impression that the language you use is the one important parameter with all the leverage when most people are working with hundreds or thousands of parameters. The art of software design is a quest to make robust analogies that transcend computation substrates and specific technologies.

With that in mind, here's some stupid programming language discourse.

What would your programming language of choice be if there were no real-world constraints to contend with? Another way to put it—what is your default programming language?

For me it is Clojure.

Here's a distillation of the affordances that make me like Clojure.

Simplicity, ergo, stability

One of my biggest issues with modern software is the compulsion to change, tweak and update things. I detest how something or the other breaks when I run an update. But updates are sometimes needed. In my ideal world, software should be “write once, maintain less”. The way to have this is to provide simple but powerful tools that adapt to changing requirements, without needing to churn code and break things.

How do languages themselves deal with new requirements? If you are willing to entertain my flagrant simplifications, there are two paths that most languages appear to take.

Clojure has opted for the second path. However, the compromise is tiny because of the affordances provided by its syntax and evaluation model. The philosophy is to provide a minimal set of features that are also terrifically expressive. Another way to say it is that Clojure favours Simple over Easy. Less is more. By having a small, rock-solid, extremely expressive core, there is no need to tweak the language implementation to improve ergonomics or add features. With such high levels of expressiveness, things that are typically shipped as new language features or complex frameworks show up as mere libraries in Clojure.

You can write as sophisticated a system with dramatically simpler tools, which means you're going to be focusing on the system, what it's supposed to do, instead of all the gook that falls out of the constructs you're using. — Rich Hickey, creator of Clojure

Here are graphs that visualise code churn over time in the repositories of Clojure and Scala. [Source: A History of Clojure, HOPL]

clojure code retention graph.

scala code retention graph.

This ethos is also reflected in libraries provided by the community—it's fairly common to see popular libraries have their files untouched for years. It's not because the project is dead—it's because it's stable and solved the problem effectively.

What are these affordances that make Clojure so expressive, and thus stable at its core?

The design is inspired by Lisp languages. These are among the oldest and most well-understood programming languages. The syntax is a grouping of parentheses that closely map to abstract syntax trees. There's very little between the language you are writing and the representation that's being evaluated. Lisp can understand its syntax, ie, you can write procedures (called macros) that accept syntax (which is just a list) and return modified syntax (by manipulating and reordering the list). This allows a developer to create syntactical affordances without the need to modify the runtime. Other languages have metaprogramming too, but it's much easier to do this with a Lisp syntax.

Macros are a rather advanced use-case, and I have rarely had to write my own, but it's nice to have this timeless core that rarely changes, yet is open to extensions like CSP and logic programming should one need them. In my experience, the parens-heavy expressions-only syntax is plenty enough to represent most things I need.

Like many Lisps, Clojure encourages a style of programming that is declarative, avoids mutable states and keeps things simple. Your data is represented as data, not unwieldy objects or structs. It is operated on by many stateless functions, and not by methods coupled to the data. There is little friction in representing your problem space effectively.

This is possible in other languages, but Clojure makes this simple style easy.

To (not rigorously) demonstrate a simple functional style, here's a program that groups anagrams from a list of words. I generated the code with GPT-4, which is trained on a lot of openly available code—I expect the outputs to somewhat represent the preferred programming style of each community.

The Prompt

idiomatic clojure program that takes a list of strings and returns a list with anagrams grouped. you are not allowed to sort the entire list. only provide a solution with no explanation.

followed by

can you make it better?

(defn group-anagrams [words]
  (->> words
       (group-by (comp sort seq))
       (vals)
       (map set))

(group-anagrams ["veer" "lake" "item" "kale" "mite" "ever" "pale"])
;; => (#{"veer" "ever"} #{"lake" "kale"} #{"item" "mite"} #{"pale"})

How would this look in Python? I repeated the same prompts above, substituting “clojure” with “python”:

def group_anagrams(strs):
    anagrams = {}
    for word in strs:
        # Using a sorted string as the key
        key = ''.join(sorted(word))
        if key in anagrams:
            anagrams[key].append(word)
        else:
            anagrams[key] = [word]
    return list(anagrams.values())

group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"])
# [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]

To me, the Clojure code looks much better (although there's room to improve^). I am not procedurally running mechanical tasks in my head as I read the code—I can easily see the intent. It's also a lot more maintainable (although it won't matter much here), as each part of the process is cleanly untangled and composed together. This is not so with Python•. I'll leave comparing prompt results from other languages as an exercise for the reader.

(PS: If you find another language giving you an elegant solution, feel free to share it with me on mastodon @[email protected]).

Clojure also affords extremely tight feedback loops thanks to REPL-assisted development. The REPL is an alternative interface that lets a developer inspect and run parts of their program while it is running. The REPL has been a great scratchpad to quickly try out different implementations, debug things or replay parts of my program (which works well when most code is functional).

I cannot demonstrate how great this is in a text medium, so I shall point you to this short demo that barely scratches the surface.

There are a lot of other reasons why people like Clojure—concurrency is a first-class concern (STM, CSP, refs, agents etc), it has multimethods, a really nice polymorphism implementation that solves the expression problem, contract systems, etc. These are parts of Clojure I have not had a lot of experience with, so I shall refrain from commenting on them.

The vibe

Clojure is a language that provides powerful, expressive and ergonomic primitives to do more with less. These primitives are simple, even if not the easiest. It feels like a better fit for small teams of well-trained software craftsmen over large teams of poorly-trained mediocre programmers.

With great expressiveness comes greater responsibility to use it well. Like with all languages, it's not hard to produce garbage even with Clojure.

I was fortunate to have access to resources and mentors who improved my craft through Clojure. Even though I don't write much Clojure anymore, its ideas have been baked in for me and I'm better at designing programs because of it.


Appendix: More about me

Another part of stupid programming discourse is that the person who is dispensing an opinion is very relevant. When I see a person say they love Rust for its static types and memory safety, I'd process the opinion differently based on whether this person writes low-level aeroplane control software or CRUD web applications.

To help you process your opinion on my opinion, here is a little bit about me:


Footnotes

[^] Those of you who know Clojure would have realised that the generated function can still be simplified further:

(defn group-anagrams [words]
  (->> words
       (group-by sort)
       (vals)
       (map set))

Another way to do it without needing to sort each word:

(defn group-anagrams [words]
  (->> words
       (group-by frequencies)
       (vals)
       (map set)))

We can also omit the (map set) if we are okay with a group of ordered lists (like the Python solution was).

One might argue that Clojure gets an unfair advantage by providing a group-by function. I'd say that's a compliment to the standard library. But even if group-by didn't exist, a Clojure user would write an implementation and use it in the way above. If you understand functional fold/reduce, this implementation also does not require mental symbol-shunting:

(defn group-by [func coll]
  (reduce (fn [m i]
            (assoc m
              (func i)
              (conj (m (func i) []) i)))
          {}
          coll))

[•] And probably a lot worse with Java. I avoided comparisons with Java because it felt almost unfair.