When I first started programming I tought the order of function parameters was essentially arbitrary; just the order the author decided to structure it. But after dabbling with a number of functional programming languages I came to the conclusion that the order is in fact important.
Let me try to convince you.
When programming Clojure, you often end up chaining function calls, like
(baz (bar (foo x)))
, since after all, we all work on data and that data is
processed by functions. This has been totally normal, at least until the
threading/thrush operator came along, ->
(a really long time ago). So you can
replace the code with (-> x foo bar baz)
and it looks much cleaner and
obvious. It’s fantastic!
This only works for functions with the arity of 1 (single argument functions)
and by extension, for functions which take the element to be “threaded in” as
their first argument. Therefore, Clojure also has also its sibling, ->>
(called “thread-last”, analogous to ->
being “thread-first”), which
unsurprisingly threads the value in as last argument to the specified
functions.
Working with code, we often have a seq
that we want to operate on, so that’s
what we usually thread. Unfortunately, the standard library is not very
consistent about this, since common seq
operations take the collection as
first argument, like update
, assoc
, dissoc
, conj
. So we could use
them with ->
. But then when we want to use some combinators like map
,
filter
, reduce
, the collection has to be provided last, which would
require ->>
instead.
The reason why e.g. assoc
has the collection first is that is a multi-arity
function and can associate multiple values at once, so the order of arguments
of (assoc coll :arg1 val1 :arg2 val2 :arg-n val-n)
is logical. Generally,
most clojure.core
functions which take collections and an unspecified amount
of arguments seem to be this way, which is understandable considering how
& arguments
are handled in Clojure.
To avoid the awkward mess of mixing code that uses ->
and ->>
, Clojure 1.5
introduced as->
, which allows naming the argument to be threaded (I usually
go naming the argument <>
, aka “diamond”), so it can be put in the proper
place to be resolved, but this feels very much like a clumsy (albeit effective)
compromise to get around the argument order mess.
So, which “side” of the ->
/->>
split is right? Personally, I subscribe to
the thread-last school of argument order. This means that I order the arguments
in functions according to their specificity: from the most general to the least
general. Consider (map f coll)
, which takes the function first (since it
might work on any coll
element) and then only the specific values to be
applied on. Similarly reduce
. Working this way also has the advantage that
partial
can be used to pre-populate some arguments with known values and then
just operate on a function of lesser arity.
This approach is not without precedent. For languages with implicit currying like OCaml or Haskell this order is completely normal. Currying creates out of a function like
1 2 |
|
a function like
1 2 3 |
|
So when calling (foo bar)
a function is returned which takes baz
and
returns the result. So basically it’s like using partial
for every argument.
This of course means that arguments can only be supplied left to right. The
OCaml way of threading is then coll |> map inc
so the argument is threaded in
at the end, just like our friend ->>
does. The actual reason for this is of
course a bit different, since map f
returns a single-arity function so it
doesn’t really matter whether threading first or last element, since they are
identical in that case.
A more accurate translation to Clojure would be
1 2 |
|
Which is silly, since we can just use the less awkward ->>
in this case:
1 2 |
|
So, I definitely recommend preferring ->>
as it leads to more reasonable
argument order that can better be composed with other functions. Unfortunately,
we can’t just be all happy using ->>
as we’ll have to keep using ->
for
functions like assoc
/dissoc
. Maybe having them with multiple arity was not
such a great idea to start with.