The Hyperpessimist

The grandest failure.

Finding the Optimal Parameter Order

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

(defn foo [bar baz]
  (+ bar baz))

a function like

(defn foo [bar]
  (fn [baz]
    (+ bar baz)))

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

(-> coll
    ((partial map inc)))

Which is silly, since we can just use the less awkward ->> in this case:

(->> coll
     (map inc))

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.