Readable Clojure
tonsky.me
tonsky.me
There's a reason why (seq coll) is a standard Clojure idiom, which the source for empty? reveals:
(defn empty?
"Returns true if coll has no items - same as (not (seq coll)).
Please use the idiom (seq x) rather than (not (empty? x))"
{:added "1.0"
:static true}
[coll] (not (seq coll)))
By using (not (empty? coll)), you are effectively writing (not (not (seq coll))), which isn't really very elegant, even if of marginal significance performance-wise.Curiously, he later champions the use of (some? x) rather than the arguably more readable (not (nil? x)), even though in this case the former is directly equivalent to the latter:
(defn some?
"Returns true if x is not nil, false otherwise."
{:tag Boolean
:added "1.6"
:static true}
[x] (not (nil? x)))
> correct, error-proof way to choose "first non-nil value" (cond
(some? a) a
(some? b) b
(some? c) c)
I'm sorry, but that's horrible. What if you had 20 items to check? Or an indeterminate number?How about:
(first (remove nil? [a b c]))Regarding the last example: I would say
(->> [a b c] (remove nil?) first)
is even better. However, if that vector is effectively a tuple (so a, b, and c are heterogenous and can be named), cond is more explicit and readable.I don't understand. Could you illustrate with an example?
But the construct with remove and first can be applied to a limited number of variables with meaningful names just as easily as the cond construct.
Plus it has these advantages:
- It's shorter.
- It avoids jarring repetition.
- It's more readable, I'd argue, than the cond version: in the threading-macro version you offered, it translates directly into English as "take a sequence, strip out the nil values, and return the first item of what's left".
- It can be applied equally in situations where the number of items is known at compile time, and those where it isn't.
- It can be applied to an arbitrarily large number of items.
(or a b c)But I agree: if you were ever in a position where you needed to do this, you might want to reconsider how you were representing your data.
* Use consistent, unique namespace aliases - agree. This helps tremendously when consistent across projects given the varying tooling capabilities. We even have a dictionary of namespace -> alias mappings that is expanded as new commonly used namespaces appear.
* Use long namespace aliases - agree partially. I have a few favourite namespaces present in pretty much every project that get a single letter alias.
* Choose readability over compactness - agree partially. Another part of the solution is keeping the functions small and all the types explicit. However, there's a fine line between that and having to use something like a hungarian notation for the local variables.
* Don’t rely on implicit nil-to-false coercion - agree. However, I never find myself in this situation. Mostly because I just don't use plain booleans. Pretty much always you can use an enum (keyword) instead to better express the intent. When used locally - in the scope of a single function - I find that boolean-nil problem doesn't cause any issues.
* Avoid higher-order functions - agree completely. `comp` and `partial` in Clojure are awkward. If you find yourself using them, you're probably nesting too many lambdas with hash (#) notation - move some of them out into a `let`.
* Don’t spare names - agree partially. The suggestion is definitely more readable. I just love writing threading expressions.
* Don’t use first/second/nth to unpack tuples - agree completely.
* Don’t fall for expanded opts - agree completely.
* Use * as prefix for references - agree. This needs some sort of a blessed reference in the Clojure documentation. Something to syntactically mark constants, e.g. `+constant+`, something to mark refs, e.g. `+ref`.
* Align let bindings in two columns - this is purely a matter of preference. I don't care either way.
* Use two empty lines between top-level forms - also a matter of preference. I prefer a single line.
I find that if they're not aligned and the identifiers are of varying length, that the names and code bleeds together making it hard to read which are the names and what is part of the code. Its especially bad if the code for a binding is more than one line long (which should be avoided, but isn't always possible without factoring it into a function)
(defn foo [mA]
(:foo mA))
will be passed a map in mA but at least you are drawing a boundary.I prefer clojure.spec to guard significant (ns, api) boundaries.
Advise against higher order functions. Higher order functions separate clojure from other languages, and is one of defining features of this class of languages.
Advise to not use threading. Threading is just a series of steps. I find it very readable. If I start using symbols the step sequence can become a step graph FWIW.
At a glance I know that (comp f g h) and its flat list of arguments expresses a simple function composition chain. Seeing a #(f (g (h %))) where (comp) will do makes me sift through more parens; more than that though, when (comp) is embraced as convention, a #() is an immediate cue that simple function composition is not what's being expressed, which draws me in necessarily to examine the particular structure of the #() body.
I actually think he has a point on those two in the context of Clojure specifically. The process of explicitly currying or composing functions (again, in Clojure specifically) is usually more verbose and IMO harder to read than just writing an anonymous function that expresses the same thing.
Granted most of my Haskell work is in boring line of business apps, so maybe partial application is more important in other fields.
For example, I very often write functions that take an initial parameter representing some sort of customisable context and then further parameters that are the right types to use with some standard higher order function like a map, filter or fold. Partial application then supplies the context and gives back something ready to pass into that HOF.
It's incredibly convenient to use, but sometimes when I look at 'idiomatic' React code, in particular in the context of Redux, I can't help but feel it's a bit too much, especially when I look at the code through 'teacher' eyes, and especially when it's combined with JSX. And that's not even mentioning the arrow function variants, default parameters, and other ESNext goodies.
And yet I find it difficult to keep my hands off it, because it's just so damn convenient.
"Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. "
-- Antoine de Saint-Exupery
I just find the "plain_fn <$> weird_type <*> weird_type ..." e.t.c really convenient.
I don't know if that's true anymore. HoF are becoming mainstream - and rightly so.
Java, C++, Python, C#, VB.NET, Javascript, PHP, Perl, Ruby.. they all have them.
But every corporate programming language has lambdas you can pass around as data now.
(def good-set-of-stuff
(->> stuff
(filter good?)
(map enrich)
(into #{}))
than it is to read: (def good-set-of-stuff
(into #{}
(map enrich
(filter good? stuff))))
or (def good-set-of-stuff (into #{} (map enrich (filter good? stuff))))Threading enhances readability so long as it is not overused.
(let [!input (atom {:some "data"}]
(emit! ::event @!input) ;; @ and ! always go together
(reset! !input {:new "data"})) ;; mutations against plain symbols "looks wrong"However, right now I'm also working with Java (unfortunately :D) and !input looks a bit too similar to a not condition, so adds a tiny bit to my language context switch.
Infix is occasionally more readable for math, but I'd rather have a macro to transform a delimited section of infix code than to mess with the language.
Also, I don't want to live without Paredit (or Smartparens)
Instead of in this example write `[cognician.chat.dom :as dom]` just write `cognician.chat.dom`. Then when calling, use the whole namespace.
It's a tradeoff between typing a little bit more (in reality, using the autocomplete function) in exchange for having the code readable. When you read something on line 213, you don't have to scroll up to double check what that short name refers to.
Wish :as was never added to begin with.
I don't necessarily want to comment on the particular details here. You may agree or disagree with elements of any one particular style guide; many depend on your team and audience.
With that said, I would make these comments. First, your team might find value in choosing an existing style guide and updating it as you go. Second, it might use pairing as a way of letting these decisions happen organically.
> - use contains? instead of using sets as functions,
[snip]
> An example. To understand this piece of code you need to know that possible-states is a set:
(when (possible-states state)
... )
> By contrast, to understand following code you don’t need any context: (when (contains? possible-states state)
... )
I disagree. In the first example, it's clear from context that possible-states is a function (to be precise, an object that implements IFn) that returns a truthy or falsy value depending on the value of state; the name possible-states suggests it's checking that the value of state is valid, according to some criteria.To determine what those criteria are, you'd have to look at the definition of possible-states: but that would also be true even if you used the more verbose contains? construct.
By not using contains?, you also retain the option to replace possible-states with an actual function, should you later discover you need further validation or processing not possible with a simple set.
For example, if state is a text string, and you find out further down the line that sometimes it's in the wrong case or has unwanted leading or trailing white space, you can replace
(def possible-states #{"foo" "bar" "baz"})
with (defn possible-states [state]
(->> state
clojure.string/trim
clojure.string/lower-case
#{"foo" "bar" "baz"}))
without needing to change code elsewhere.Even if you don't feel that this flexibility is worth the ambiguity, contains? offers little comfort, as it can take many things other than a set:
(contains? #{"foo"} "foo") ; true
(contains? {"foo" 1} "foo") ; true ("foo" is a key of the map)
(contains? {:bar "foo"} "foo") ; false ("foo" is a value but not a key)
(contains? ["foo" "bar"] 1) ; true (vectors are keyed by integers)
(contains? '("foo" "bar") 1) ; false (but lists aren't)
(contains? "foo" 1) ; true (but strings are)> I do it by hand, which I consider to be a small price for readability boost that big. I hope your autoformatter can live with that.
Cursive's formatter has this option.
, f l
inside clojure buffers.It also has downsides (overly large diffs if you need to adjust alignment after adding a new binding-with-a-long-name to a let clause).
I also find two empty lines between functions to be too much for my taste.
Lining up columns like this can make you crazy. There's a good example of this earlier on the page:
(ns examples.long-aliases
(:require
[clojure.spec :as spec]
[clojure.test :as test]
[clojure.java.io :as io]
[rum.core :as rum]
[diatomic.api :as diatomic]
[clojure.string :as string]
[cognician.chat.util :as util]
[cognician.chat.server :as server]
;; you can use dots in aliases too
[cognician.chat.server.schema :as server.schema]
[cognician.chat.ui.entries.core :as ui.entries.core]))
Here we have two aligned sections, one above and one below the ;; comment. Why aren't they all aligned to the same column? Apparently it's the ;; comment that says "OK, you can stop worrying about alignment here and start a new alignment section below this line."Now what if we remove the comment?
(ns examples.long-aliases
(:require
[clojure.spec :as spec]
[clojure.test :as test]
[clojure.java.io :as io]
[rum.core :as rum]
[diatomic.api :as diatomic]
[clojure.string :as string]
[cognician.chat.util :as util]
[cognician.chat.server :as server]
[cognician.chat.server.schema :as server.schema]
[cognician.chat.ui.entries.core :as ui.entries.core]))
Well, that won't do. Time to realign everything to keep it clean: (ns examples.long-aliases
(:require
[clojure.spec :as spec]
[clojure.test :as test]
[clojure.java.io :as io]
[rum.core :as rum]
[diatomic.api :as diatomic]
[clojure.string :as string]
[cognician.chat.util :as util]
[cognician.chat.server :as server]
[cognician.chat.server.schema :as server.schema]
[cognician.chat.ui.entries.core :as ui.entries.core]))
Now we see another problem with alignment. The sheer amount of horizontal white space makes it hard to visually match up the first several lines that have much names on the left.And how do we decide what to align and what not to align? We're lining up the :as, but why not the closing brackets too?
(ns examples.long-aliases
(:require
[clojure.spec :as spec ]
[clojure.test :as test ]
[clojure.java.io :as io ]
[rum.core :as rum ]
[diatomic.api :as diatomic ]
[clojure.string :as string ]
[cognician.chat.util :as util ]
[cognician.chat.server :as server ]
[cognician.chat.server.schema :as server.schema ]
[cognician.chat.ui.entries.core :as ui.entries.core]))
Plus, alignment obviously works only in a monspaced font. I like to make my code readable whether someone uses a monospaced or proportional font. If you forgo alignment, code is equally readable in any kind of font.And really is non-aligned code any less readable than the aligned version? It may not be quite as pretty, by some definition of "pretty", but does it really matter? And it has the advantages of not requiring constant fiddling as you add names, not messing up your VCS diffs, and keeping the left and right parts of the :as close together so you can easily match them up visually?
(ns examples.long-aliases
(:require
[clojure.spec :as spec]
[clojure.test :as test]
[clojure.java.io :as io]
[rum.core :as rum]
[diatomic.api :as diatomic]
[clojure.string :as string]
[cognician.chat.util :as util]
[cognician.chat.server :as server]
[cognician.chat.server.schema :as server.schema]
[cognician.chat.ui.entries.core :as ui.entries.core]))Now, I do think this is of rather limited value. So I wouldn't die on this hill. But, column alignment is clearly superior to my subjective eye. It is a shame not everyone has align-regexp.
Definitely subjective, though, I guess.
(ns examples.long-aliases
(:require
[ clojure.spec :as spec ]
[ clojure.test :as test ]
[ clojure.java.io :as io ]
[ rum.core :as rum ]
[ diatomic.api :as diatomic ]
[ clojure.string :as string ]
[ cognician.chat.util :as util ]
[ cognician.chat.server :as server ]
[ cognician.chat.server.schema :as server.schema ]
[cognician.chat.ui.entries.core :as ui.entries.core]))- Lint: https://github.com/candid82/joker - Gofmt: https://github.com/pesterhazy/boot-fmt