top | item 14413872

Readable Clojure

258 points| tosh | 8 years ago |tonsky.me

77 comments

order
[+] dustingetz|8 years ago|reply
Eh I dunno. Clojure comes closer than any other programming language to being like spoken language in that there are tons of different tenses and conjugations and a large, expressive vocabulary to articulate precisely what you mean. Like when writing in English - decide who your audience is and then use the vernacular they know to the full extent. In any serious project, that pretty much means you get to assume at least that they are fluent in clojure, before adding in other DSLs like funcool/cats (category theory) if your problem domain needs it. If your audience needs guard rails, Clojure is probably not the language for you for other reasons, for example the culture of "just read the source" (which is a great thing i think!). Just my 2c as someone who has written clojure full time for a couple years. Tonsky has probably written more Clojure than I have.
[+] dghf|8 years ago|reply
> prefer (not (empty? coll)) over (seq coll),

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]))
[+] escherize|8 years ago|reply
Must say I prefer or for this, and consider differentiating false and nil to usually be a code smell:

    (or a b c)
[+] lambdadmitry|8 years ago|reply
The way empty? is implemented is literally an implementation detail, so it's somewhat irrelevant to the discussion of readability.

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.
[+] dm3|8 years ago|reply
* Don’t use “use” - agree completely.

* 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.

[+] dkersten|8 years ago|reply
Align let bindings in two columns - this is purely a matter of preference. I don't care either way.

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)

[+] kimi|8 years ago|reply
What is wrong with Hungarian notation? I use mX and vY for maps and vectors when they are not totally transparent and it does help. Of course you have no guarantee that your

   (defn foo [mA] 
     (:foo mA)) 
will be passed a map in mA but at least you are drawing a boundary.
[+] abc_lisper|8 years ago|reply
Agree with most of them, except,

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.

[+] emidln|8 years ago|reply
I'd go so far as to advise aggressive use of the threading macros in order to remove visual nesting. It is much easier to read:

    (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))))
[+] chc|8 years ago|reply
Based on the examples presented, it seems like the OP doesn't actually mean "higher-order functions" in general. The "good" example still uses map, which is a higher-order function. It seems like what OP has a problem with is specifically functions that return functions.
[+] adambard|8 years ago|reply
I for one enthusiastically agree with the author's advice to use the `#()` syntax in place of `partial` and/or `comp`. It's just plain easier to parse, even for those who are familiar with higher-order functions but especially for those that aren't.
[+] lacampbell|8 years ago|reply
Higher order functions separate clojure from other languages

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.

[+] devin|8 years ago|reply
-> and ->> are not equal to some->, cond->, etc.

Threading enhances readability so long as it is not overused.

[+] pgt|8 years ago|reply
Like the *-prefix, I derived significant readability by prefixing all reference type symbols (like atoms) with !, which goes hand-in-hand with the exclamation mark in `swap!` and `reset!`. For example:

    (let [!input (atom {:some "data"}]
      (emit! ::event @!input) ;; @ and ! always go together
      (reset! !input {:new "data"})) ;; mutations against plain symbols "looks wrong"
[+] dkersten|8 years ago|reply
I like that. I think it reads a bit better because ! looks less like @. The analogue with reset! and swap! is nice too.

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.

[+] penpapersw|8 years ago|reply
I've been bitten before trying to use (if (some? (thing))) instead of just (if (thing)) for the sake of readability, because as (defn thing ...) evolved, it ended up returning a function call that sometimes returned false instead of just nil, so the conditions were sometimes inverting in subtle ways and it was really hard to track down why.
[+] bryanlarsen|8 years ago|reply
I was hoping that this would be an implementation of readable lisp (aka sweet expressions) for clojure.

http://readable.sourceforge.net/

[+] yjgyhj|8 years ago|reply
Disagree that it's readable. The parens are great - they show exactly where a function call starts and where it ends. The program is a tree!
[+] weavejester|8 years ago|reply
To my mind, parinfer provides the benefits of significant whitespace in Lisp without any of the disadvantages.
[+] yjgyhj|8 years ago|reply
One thing me and my teammate did in our last Clojure gig was to not shorten namespaces (with the `:as` keyword).

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.

[+] dj-wonk|8 years ago|reply
I remember having these kinds of discussions with coworkers in a previous job where I did Clojure daily.

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.

[+] draw_down|8 years ago|reply
A lot of these can be linter rules, like no single-letter aliases, or forcing common packages to be imported as one consistent name. (I don't know a ton about clojure but I'm assuming linting is a thing.) To the extent that linting keeps things consistent and helps prevent mistakes, without unnecessarily hamstringing developers, it's a good idea imo.
[+] z3t4|8 years ago|reply
Advice for the author, skip most of the "don't" if you write both the bad and the good, it will be confusing and the reader will remember both, but after some time forget which was better.
[+] dkersten|8 years ago|reply
I found the comparisons useful. It showed me why its better, instead of just saying "hey this is better, do this".
[+] yomly|8 years ago|reply
I don't disagree with you but without examples of bad the point can seem abstract in itself...
[+] dghf|8 years ago|reply
> My (incomplete) set of personal rules:

> - 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)
[+] latte|8 years ago|reply
I find the advice on prefixing atom names with * very useful, especially for Clojurescript / Reagent code. I have never encountered such style before though - is it a widely accepted convention?
[+] Plugawy|8 years ago|reply
I have seen some Common Lisp code which also uses that notation. Of course it could have been CL code written by a clojurian;-)
[+] emidln|8 years ago|reply
I haven't seen it. I don't find myself using atoms very much and when I am using atoms, it's almost always because I'm trying to use doseq when a more functional version of my code exists if I would think about it.
[+] dantiberian|8 years ago|reply
> Align let bindings in two columns

> 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.

[+] nathell|8 years ago|reply
Is it just me who doesn't find this to boost readability?

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.

[+] throwanem|8 years ago|reply
Emacs has align-regexp, if that's of use to anyone in the Clojure world. (It's of extensive use in Emacs!)
[+] Touche|8 years ago|reply
For such a young language Clojure seems to have a lot of cruft. 3 ways to import a module, what were they thinking?
[+] valw|8 years ago|reply
Clojure is almost 10 years old. I don't think this reasoning makes sense though, because from what I've seen most of the cruft in any language comes from the very early design decisions (both in what's included and what's missing).
[+] mhd|8 years ago|reply
Is that an actual editor in the screenshots? I thought most clojurists use emacs...
[+] perryprog|8 years ago|reply
I used to, but now I use Cursive in IntelliJ IDEA.
[+] joncampbelldev|8 years ago|reply
quite sensible advice, will probably start minimising my use of refer. I don't agree with all of it, but that's the nature of style guides.
[+] nerdponx|8 years ago|reply
A lot of this applies to Python as well.