Hacker News new | comments | show | ask | jobs | submit login
Readable Clojure (tonsky.me)
258 points by tosh 9 months ago | hide | past | web | favorite | 77 comments



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.


> 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]))


Must say I prefer or for this, and consider differentiating false and nil to usually be a code smell:

    (or a b c)


The relevant bit of the article was explicitly concerned with situations where false values needed to be returned if present, and ruled out the use of or for precisely that reason.

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.


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.


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


Sorry, I forgot the original snippet and though about something else (working over sequences). However, in the original context it makes even more sense to use cond as the number of variables is clearly limited (and they probably have meaningful names).


> However, in the original context it makes even more sense to use cond as the number of variables is clearly limited (and they probably have meaningful names).

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.


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


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)


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.


I'm not a big fan - once you start encoding type information in names, you have to be extremely attentive when refactoring. It doesn't scale too well in a larger team.

I prefer clojure.spec to guard significant (ns, api) boundaries.


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.


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


Without much Clojure experience, I agree. Sequential, what-comes-first, is easier to read than finding the first evaluated expression and peeling backwards just to build the same mental model.


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.


So basically he'd be against currying? I find currying useful in certain situations - threading/piping being a common scenario.


Currying and composition, I think.

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.


I frankly don't like these features even in Haskell. There are a few specific places where it makes things more elegant, but in general I dislike having to think about "how do I arrange params to allow for the greatest possible use of partial application in the rest of my code?", and then the inevitable "oh I can fix that with (flip (flip flip))" or some nonsense. It's tangential to the requirements of getting my app done, and generally time / brain cells wasted. I'd rather it simply not be part of the language in the first place.

Granted most of my Haskell work is in boring line of business apps, so maybe partial application is more important in other fields.


It's one of those things where you can take a simple tool and probably make something hideously complicated with it if you go too far, but in simple cases it's useful all over the place.

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.


If you were to get rid of curried functions everywhere, how would change the applicative pattern.

I just find the "plain_fn <$> weird_type <*> weird_type ..." e.t.c really convenient.


I would rather chunks of functionality not be removed from tools to suit other peoples aesthetic sensibilities.


Not all features have equivalent upside (I.e. Some features have very limited upside and significant cost, in terms of maintainability etc.) When creating a codebase we should be conservative with the features we use - and this is all the article is arguing for.


I feel this way about the destructuring assignment and spread syntax in javascript, or at least a little bit.

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.


I believe I've heard it said: the most difficult thing when building a language is not deciding which features to add, but which features to leave out.


This is a pretty common maxim, and applies to any design exercise :)

"Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. "

-- Antoine de Saint-Exupery


Sometimes they have different characteristics even though they produce the same result. Such as when composing transducers, which has performance implications.


I use it a lot when writing functions with side-effects in JavaScript, as a sort of informal dependency injection. If I have a function that needs to do an HTTP request, I will curry it so that the outermost function of the service takes the HTTP service as its argument.


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.


That's interesting, because I find #() rather less easy to parse compared to (comp) or (partial) for otherwise equivalent expressions.

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.


A happy medium IMO is to give the function resulting from `partial` or `comp` a name in a let binding. If it takes one arg, great; if it doesn't then the #() form with the named fn inside still a little shorter & easier to parse. This is inline with the other advice that brevity isn't the main virtue for readability.


Exactly. The author wasn't advising against using higher order functions in general, just certain ones like comp and partial, which are easily replaced by anonymous functions. I.e. (comp a b) is the same as #(a (b %)), so why not use the latter if it's easier to understand. I don't completely agree with the author on this point, but I know he wasn't against higher order functions.


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.


does C's qsort count?


I'd draw the line at that.

But every corporate programming language has lambdas you can pass around as data now.


-> and ->> are not equal to some->, cond->, etc.

Threading enhances readability so long as it is not overused.


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"


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.


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.


I was hoping that this would be an implementation of readable lisp (aka sweet expressions) for clojure.

http://readable.sourceforge.net/


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!


+1 That thing is gross. I'd edit so much more slowly if I had to go back to a messy, complex syntax. At least without a very clever / specialized editor effectively undoing the change for me.

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.


Infix is only easy to read if you're only used to it. Not inherently better - I'd say opposite even.

Also, I don't want to live without Paredit (or Smartparens)


To my mind, parinfer provides the benefits of significant whitespace in Lisp without any of the disadvantages.


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.


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.


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.


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.


I found the comparisons useful. It showed me why its better, instead of just saying "hey this is better, do this".


I don't disagree with you but without examples of bad the point can seem abstract in itself...


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?


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.


From my experience, atoms are indeed rarely needed on the backend, where you have a DB to store the app state, but you can't do pretty much anything without them in cljs / Reagent.


I have seen some Common Lisp code which also uses that notation. Of course it could have been CL code written by a clojurian;-)


These feel like things that a linter could check and a Gofmt-like tool could do. Has anyone here used such tools? A quick Google turned up these options:

- Lint: https://github.com/candid82/joker - Gofmt: https://github.com/pesterhazy/boot-fmt


Very well designed website.


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


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


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.


I second this. I actually don't find it improving readability (maybe even worsening it for me) and the unnecessary diff changes aren't useful either.


I'm with you on the column alignment. I don't see how the items in the right column have so much to do with each other that reading down the column is helpful. All that really gets aligned are the opening parentheses, and what's the point of that?

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]))


May as well go all the way:

    (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]))


If you're going to align things I think this is actually the way you should do it, having the alias so far away from the name really hurts readability in my opinion.


Sadly, for me you made the opposite point. The column aligned sections were far easier to see the sections than the other.

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.


Nothing to be sad about there. I'm glad you found the examples useful, even if we drew different conclusions from them.


Could it be a matter of syntax highlighting? I find highlighting draws the subjects out to me better. In my case the :as would be a different color and the closing bracket a muted color, which makes it easy for me to pick out the importing names.


Not for me. My code is syntax highlighted, but I still personally find the aligned code easier to see. The difference is between having all of the stuff side-by-side (vertically) and looking for colours inside other code fragments.

Definitely subjective, though, I guess.


Emacs has align-regexp, if that's of use to anyone in the Clojure world. (It's of extensive use in Emacs!)


clojure-model.el has clojure-align to do this. In spacemacs it is bound to

    , f l
inside clojure buffers.


That works for maps and many things, but I've had good use on other types of forms. Really a big fan of align-regxp.


For such a young language Clojure seems to have a lot of cruft. 3 ways to import a module, what were they thinking?


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


Cruft in a language isn't such a big deal as long as it is fixed. What really matters is cruft in the ecosystem. There are so many Clojure tutorials that use "use" and "refer". That's the real problem. JavaScript has with(), but there are approximately 0 articles online that use it, so its not an issue.


Is that an actual editor in the screenshots? I thought most clojurists use emacs...


I used to, but now I use Cursive in IntelliJ IDEA.


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.


A lot of this applies to Python as well.




Applications are open for YC Summer 2018

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: