Speaking with a LISP
When I started learning Clojure, one of the things I found most difficult was simply reading the code.
Lots of other people seem to struggle with this too. There are plenty of people around the web echoing this sentiment or debating it at length, and several of my friends have made similar remarks to me in person.
There are techniques one can use to make the code more readable, like naming values with binding expressions or using macros, such as the threading macros. This Gist (by none other than Rich Hickey himself) is a great example.
But even code that follows these techniques can still be hard to read for a lot of people.
Let’s take a simple example:
(iterate inc 1)
I saw this expression in someone’s solution to an exercise on Exercism.io, and I had no idea what it meant or how it worked.
When I read the expression, I did what I think everyone does when they’re trying to understand unfamiliar code: I tried to break down the code’s execution. Mentally, I turned the Clojure form into a series of imperative steps:
- We’re calling
iterate, which takes a function and a value and returns a sequence.
- The value
1serves as the first argument to the given function,
- Then, the return value of that call serves as the next argument to another call to the same function (
- So the sequence you get back starts with the given value, 1, then would be 2 (from
(inc (inc 1)), then 3 (from
(inc (inc (inc 1)))) and so on forever.
(iterate inc 1) means: create an infinite sequence that starts with 1, then increments each step.
The end result is simple enough, but that was a lot of steps to work out!
This is a fundamental impediment to understanding Clojure code: it’s not structured in a procedural fashion, so it’s difficult to understand procedurally.
So that’s it I guess. Clojure is hard. Let’s give up.
No, do not be deterred! Clojure may not be easy to understand procedurally, but that’s just because it’s not a procedural language. We need to stop thinking procedurally and, instead, learn to think on Clojure’s terms.
Learning the language
Learning a new programming language is a lot like learning a new spoken language. I took Spanish in high school and college. When I first started learning, I broke sentences down to the word, then translated the words, then put the sentence back together again in English.
Let’s again take a simple example:
Me llamo Nathan.
When I started learning Spanish, I broke this sentence down like this:
- Translate the words:
- “Me” => “to me”
- “llamo” => “I call”
- “Nathan” => “Nathan”
- Put it together: “To me I call Nathan”
- Make sense of it all: “I call myself Nathan”, or in English parlance, “My name is Nathan”
I was breaking down the sentence piece by piece, just like I was when trying to read that Clojure code, and again, that required a lot of mental effort. The process is tedious and difficult.
But over time, I began to understand Spanish at a higher level. I no longer needed to stop and translate every word. Phrases like “me llamo” became ingrained to the point that I didn’t even have to think about what they meant. Previously difficult areas like verb conjugation and noun genders began to flow from my tongue without pause. Eventually I could carry on full (albeit, simple) conversations with native speakers.
This is happening for me in Clojure as well.
From fragments to structures
These days, when I read forms like
(iterate inc 1), I don’t have to stop and break it down step by step. I see that form and immediately understand it means: “Count up from 1”.
(reduce + coll) means “Sum up
((juxt :last-name :first-name) people) means “Get the last and first names of the people”.
(when-let [remaining (seq (filter even? nums))] (prn remaining)) means “If there are any even numbers, print them out”.
These patterns are idioms of the language, just as “me llamo” is an idiom in Spanish. Once you learn them, you will understand their usage in other contexts as well. Just like with spoken language, idioms help you apply a prior understanding of something to a new situation.
The power you get from this is transferrence among codebases. Just like real life, code bases have different jargon for their domain (e.g. different function names, macros, namespaces, etc), but because letters and words and grammar is all shared (like syntax, functions, data structures, immutability) it’s pretty easy to jump right into conversation with someone else about their domain (like starting to contribute in a different codebase).
Furthermore, it’s much easier to compose these elements of the language into new sentences, or to re-word your sentence a different way to improve its clarity. Working with objects is more like trying to have a conversation via email (or something….?) – you have to pre-plan what you’re going to say, and it’s hard to backtrack, to fluidly adapt as you’re speaking, etc.