In July 2017, I found myself editing some Clojure code that looked approximately like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
This route handler validates its inputs, and if they fail validation,
then it returns an error response. I found this pretty ugly. This
small chunk of code has numerous if
branches and quite a bit of
nesting. All of this makes it hard to read and hurts understanding.
While adding a new feature to it, I remembered some code I wrote with
Case back in late 2015. Back then we were
working on Lumanu and wrote a Clojure macro that we called
halt-on-error->>
. This macro worked similarly to ->>
, except it
allowed any step in the processing pipeline to halt execution and
trigger an error handler. We were working on a web crawler at the
time, and this macro significantly improved the readability of our
data processing pipeline. There was a lot of error handling code
throughout the web crawler, and this macro helped keep it readable.
I realized that using a similar macro would make this code easier to
follow. I recreated halt-on-error->>
to allow any form to cause it
to return early. The above code could then be written like below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Once you understand halt-on-error->>
, this chunk of
code is much easier to read.
Let’s implement halt-on-error->>
.
Implementing halt-on-error->>
Here are some tests for that specify how halt-on-error->>
should work.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
Below is an implementation of halt-on-error->>
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
So what is this macro doing? First, it uses gensym
to get a symbol
with a unique name and stores this in g
. It then defines a helper
function called pstep
for use in the code generation part of the
macro.
This macro generates a let
block that repeatedly executes a form and
assigns the return value back to g
. g
is then checked to confirm
execution should continue before it is threaded into the next form. If
g
is ever an instance of a Stopper
, execution halts and the value
wrapped in the Stopper
is returned.
Looking at an expanded version of a macro can be easier to understand than a written explanation. Below is a macroexpanded version of one of the tests.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Looking at that expansion, you can see how we are using a let
block
to repeatedly assign to the same symbol and we check that return value
before executing the next stop.
This isn’t a new pattern. There are libraries that implement similar ideas. At IN/Clojure 2018, Varun Sharma gave a talk about how this cleaned up their code. You can even get bogged down and throw around words like monad when talking about it.
I’d encourage you to look at your code and see if you have areas where error handling code is detracting from the readability. This might be an area where this, or something similar to it, would help.