·7 min read

It compiles. That doesn't make it Clojure.

A codebase where every function is correct and almost none of it reads like Clojure. The syntax ported over cleanly; the idioms didn't come with it.

Plenty of Clojure compiles cleanly and still doesn’t read like Clojure. The functions are correct, the tests pass, and most of it reads like it was written in another language and translated across a word at a time.

That’s the thing about a Lisp. The syntax is so small that you can bring your previous language with you and nothing stops you at the door. if, count, instance?, try — the vocabulary is all there, so you can write Java in parentheses for years without ever seeing a compiler error.

Which is the question worth asking: what is the point of writing Clojure as Java, or as Python, or as JavaScript? If the result is your old language with different brackets, you’ve paid the cost of a new language and taken none of the benefit.

Idiom isn’t taste

“That’s not idiomatic” usually gets heard as a preference, and preferences are arguable. But idiom is closer to a compression scheme that both sides already agreed on.

When I read (seq items) I know the shape of the answer before I finish the line. When I read (> (count items) 0) I have to reconstruct it from the mechanics — count the elements, compare against zero, ah, you meant any. Both are one line. One of them I read; the other I decode.

That decoding cost is paid by every future reader, which is mostly you, six months out. None of what follows is about anybody being wrong. These are the habits everyone arrives with, and the only fix is knowing they’re habits.

The Java accent

(v/defvalidator is-boolean?
  {:default-message-format "%s must be a boolean" :optional true}
  [value]
  (instance? java.lang.Boolean value))

Two Java habits made it through intact. isBoolean() became is-boolean?, and instanceof became instance?.

The name is the easier one — the style guide’s Predicate Methods rejects both is-palindrome (Java) and palindrome-p (Common Lisp) in favour of the trailing ?. The ? is already doing the work is was brought along to do.

The body is the more interesting one, because Clojure has this exact check, named. From clojure/core.clj:

(defn boolean?
  "Return true if x is a Boolean"
  {:added "1.9"}
  [x] (instance? Boolean x))

That’s the whole function. So the rewrite isn’t a matter of preference — it’s using the name that already exists for the expression that was written out longhand:

(v/defvalidator boolean-flag?
  {:default-message-format "%s must be a boolean" :optional true}
  [value]
  (boolean? value))

Same for string?, map?, int?, number?. Reaching into interop when a core predicate exists makes the reader context-switch out of Clojure and back for no gain.

The other Java habit is reaching for a getter:

(get account :currency)   ; more verbose than necessary
(:currency account)       ; good

That’s Keywords as Functions for Map Values Retrieval, which also flags a third form worth knowing about — (account :currency) works, right up until account is nil and you get a NullPointerException instead of the nil you’d have got either other way.

The Python accent

(filter #(> (count (:items %)) 0) accounts)

This is if len(x) > 0: with the parentheses moved. The Clojure question isn’t “how many items” — it’s “are there any”, and that’s seq:

(filter (comp seq :items) accounts)

The guide files this under a name I’d never have guessed to search for: Nil Punning — “use seq as a terminating condition to test whether a sequence is empty”. Worth knowing the name; the rule is invisible otherwise.

To be fair to count, it’s O(1) on vectors, maps and sets, so this usually isn’t a performance bug. It’s a reading bug — except on an unrealized lazy sequence, where count has to walk to the end to answer a question seq answers from the first element.

Then there’s the other Python import, except: pass:

(defn parse-quantity
  [raw]
  (try
    (let [n (Integer/parseInt raw)]
      (when (pos? n) n))
    (catch Exception _ nil)))

Since 1.11 that whole shape has a name, and again it’s in core:

(defn parse-long
  {:added "1.11"}
  ^Long [^String s]
  (if (string? s)
    (try (Long/valueOf s) (catch NumberFormatException _ nil))
    (throw (IllegalArgumentException. (parsing-err s)))))

Look at what it does differently. A bad string returns nil — that’s the case you meant to handle. A wrong type throws, because that’s a bug, not bad input. The hand-rolled version flattens both into nil, which means the map you accidentally passed comes back as “invalid quantity” and you find out somewhere else entirely.

(defn parse-quantity
  [raw]
  (when-let [n (parse-long raw)]
    (when (pos? n) n)))

One difference to weigh before swapping: parse-long returns a Long, so inputs above Integer/MAX_VALUE now parse where they used to throw. Related, on the catch itself: Catching Throwables is about Throwable rather than Exception, but it’s the same instinct — a catch clause wide enough to swallow things you never meant to handle.

The habits that aren’t from anywhere

Some of it is just not having met the tool yet.

(remove #(= % :cancelled) statuses)   ; bad
(remove #{:cancelled} statuses)       ; good

A set is a function of its elements, so it is the predicate. That’s Set As Predicate, and it’s one of the moves with no equivalent to import from anywhere else — there’s nothing to translate, you either know it or you write the lambda.

The same goes for a let block whose only job is to unpack a map:

(defn line-total [item]                             ; bad
  (let [qty   (:quantity item)
        price (:unit-price item)]
    (* qty price)))

(defn line-total [{:keys [quantity unit-price]}]    ; good
  (* quantity unit-price))

That first version is the object-into-locals move, and it hides the interesting part: which keys this function actually needs. Destructuring puts that in the signature, where a caller reads it. It nests, too, which is where the let version starts to sprawl:

(defn ship-to [customer]                                    ; bad
  (let [addr (:address customer)
        city (:city addr)
        code (:postcode addr)]
    (str city " " code)))

(defn ship-to [{{:keys [city postcode]} :address}]          ; good
  (str city " " postcode))

Two levels is about the limit. Past that the binding form stops being readable and a named let is the kinder option — and let was never the problem anyway when it’s holding a computed value rather than renaming a key.

Naming has its own conventions, and they’re load-bearing in a language where you grep more than you navigate:

(defn to-year-month [d] ...)      ; not so good
(defn renewal->quarter [d] ...)   ; good

Conversion Functions: use -> rather than to. It’s a small thing that makes every conversion in the codebase findable by searching for one character sequence.

Where the guide is silent, read the source

The style guide doesn’t cover everything, and it’s smaller than people assume — there’s no section on some?, none on name, none on type predicates. That’s not a gap so much as a division of labour, because for this kind of question clojure.core is the better reference anyway. It settles the argument outright:

(defn some?
  "Returns true if x is not nil, false otherwise."
  {:tag Boolean :added "1.6" :static true}
  [x] (not (nil? x)))

So (not (nil? value)) isn’t like some?. It’s the definition of some?, inlined by hand. There’s no preference to debate.

The same trick catches the reverse mistake, where you assume core is more forgiving than it is. Stripping the colon off a keyword with (subs (str k) 1) should be name — but check before you decide name handles everything:

(defn name
  "Returns the name String of a string, symbol or keyword."
  [x]
  (if (string? x) x (. ^clojure.lang.Named x (getName))))

Strings pass through. Anything that isn’t Namednil, a number — reaches .getName and throws. So (if (keyword? k) (name k) k) keeps its guard, and the tempting collapse to (def label->string name) quietly changes behaviour at the edges.

Reading the source takes about as long as searching the guide, and it answers questions the guide was never going to.

The minute before you write

None of this is exotic knowledge. It’s a minute of hesitation before writing the line — you know the syntax is fine, so instead ask whether it’s the Clojure way of saying it, and whether the thing you’re about to spell out already has a name.

Coming from another language, that minute is how you stop transliterating. Already writing Clojure, it’s how you notice the shape you’ve been reaching for out of momentum since your last language.

It compiles either way. That was never the bar.

← all posts