Skip to content

Instantly share code, notes, and snippets.

@sovelten
Created February 5, 2020 20:34
Show Gist options
  • Save sovelten/486602d2711ed251ffee41215b7e8dbc to your computer and use it in GitHub Desktop.
Save sovelten/486602d2711ed251ffee41215b7e8dbc to your computer and use it in GitHub Desktop.
Clojure, Thread Macros, Parameter Order, Function Composition and Other Stuff

Clojure, Thread Macros, Parameter Order, Function Composition and Other Stuff

Introduction

Coming from a Haskell background and landing on Clojure-land, I had a lot of difficulty grasping some design decisions, specially regarding parameter order and the use of threading macros. This post aims to shed a light on some design decisions Clojure took and reason about whether or not, how or when should you use thread macros. This is an entirely personal opinion, but reasonable arguments are given.

Parameter Ordering

The first source of confusion is parameter ordering. In Haskell, specially due to automatic currying, it is always clear where your most important thing should go. Always last. The Haskell wiki on parameter order defines the following rule of thumb:

The more important the parameter, the more frequently it changes, the more it shall be moved to the end of the parameter list. If there is some recursion involved, probably the parameter, which you recurse on, is the one which should be at the last position.

In Clojure, the usual order you place the parameters goes somewhat like this: If your function is dealing with a collection, you place the collection as your first parameter. If your function is dealing with a sequence, you place the sequence as the last parameter. It is hard to understand exactly why this happens, and the most common answer is "because of thread-first macro". One of the best articles I found that clarifies this issue is this one. It links to an interesting discussion between Stuart Sierra and Rich Hickey himself regarding the reasons for this choice. He defines the following rule of thumb for Clojure:

Primary collection operands come first. That way one can write -> and its ilk, and their position is independent of whether or not they have variable arity parameters. There is a tradition of this in OO languages and CL (CL's slot-value, aref, elt - in fact the one that trips me up most often in CL is gethash, which is inconsistent with those).

So, in the end there are 2 rules, but it's not a free-for-all. Sequence functions take their sources last and collection functions take their primary operand (collection) first. Not that there aren't are a few kinks here and there that I need to iron out (e.g. set/ select).

Immediately we recognize there is a difference in how functions are handled in Haskell and Clojure: Haskell has automatic currying and no variadic functions. Clojure, on the other hand, has no automatic function currying but has variable arity functions. It is understandable that the approaches may differ, given these differences. However, Haskell approach produces code uniformity whereas Clojure approach give rise to ambiguous choices and code dissonance, mostly expressed by the functions that are responsible for function composition in Clojure: threading macros -> and ->>.

Moreover, this is a choice that deviates from usual Lisp implementations.

Threading macros and Function Composition

When should you use threading macros? Should you combine thread first and thread last? Certainly you can embed thread last within thread first but not the opposite because guess what, thread first and thread last receives their most important arguments first. It is not a wonder that someone decided that they needed a bunch more other arrow macros, because certainly two was not enough. But let's not forget about other standard arrow macros, such as as->, cond->, some->. With all these arrows to use, when should you use your good old function composition? It is a trivial thing to notice that what the threading macros perform are just function composition.

Thread macros leak the abstraction

The most powerful and most important feature of any Lisp is the ability to easily create macros that change the language and perform operations on code as if it were data. Many languages support macros, but Lisps are special in the sense that a Lisp program is just one of the most basic data strucrutes: a list.

But that doesn't mean you should make the regular user aware of these code manipulations. Let's lookup the definition of abstracion leak from wikipedia:

In software development, a leaky abstraction is an abstraction that exposes details and limitations of its underlying implementation to its users that should ideally be hidden away. Leaky abstractions are considered problematic, since the purpose of abstractions is to manage complexity by concealing unnecessary details from the user.

We can see that what the thread macro does is exactly that. It exposes the macro machinery to the user, screaming "Look how neat, I can transform code and inject things inside it!". Whoever wants to understand what the thread macro does has to understand is takes pieces of code missing an argument and injects the missing argument in the middle of it, when all we should actually be doing is composing functions.

Thread macros are a source of ambiguity

Since you have to use different arrows depending on the parameter order, it is never clear when you should use it, which one should you use. Maybe you should use the mother of all arrows (that marks the place where the replacement will take place), maybe you should use thread first with embedded thread last, maybe you should use function composition. Too many choices means that there is no obvious better choice.

Thread macros are confusing

When you see the name for a function you know, you often expects it to have a certain order or number of arguments. The thread macro breaks this expectation by removing the first or last argument of the function call. You look at the function and it takes a little while to rationalize that this is a thread macro and because of that the signatures don't apply because the right argument will be injected into the code when the macro is expanded.

When and how to use thread macros

All in all, thread macros are not all that bad if used in very specific contexts, considering you have to live with it. It may help to consider thread macros as a tiny DSL made for a specific purpose. For instance, thread first (->) is a DSL for transforming a collection by applying functions that change the collection. Thread last is a DSL for the purpose of transforming a sequence. Every time a collection becomes a sequence or vice-versa, it's probably the right time to end the transformation pipeline, otherwise it starts to become difficult to follow what kind of transformations are taking place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment