Howard M. Lewis Ship
Posted on September 15, 2021
Sometimes, old habits die hard. For a long time, I've done a lot of debugging in my Clojure code at the REPL, using prn
.
With Clojure, debugging by writing to the console is not as primitive as it may sound; sure, you can fire up the debugger and set a break point, but by injecting a prn
at just the right spot, and with just the right data, you often get just exactly the useful data you need to diagnose a problem.
Other times, with meatier data structures, I will break out clojure.pprint/pprint
... but this requires more work, to import the namespace, and means I also have to cleanup the ns
declaration.
Now, I noticed a ways back that Clojure 1.10 added the tap>
function, and I vaguely knew it would be helpful for this kind of thing; I finally got around to trying it out to debug some hairy NullPointerException bugs in Lacinia.
What does tap>
do? By default ... nothing. It's just a function you can pass a value to, but nothing happens when you do (it also returns true).
tap>
is hardly useful until you add a tap with add-tap; a tap is any function that takes a single argument.
Invoking tap>
will cause the function (or functions, if add-tap
is called multiple times) to be invoked asynchronously.
So if you, for example, (add-tap clojure.pprint/pprint)
each subsequent call to tap>
will be pretty-printed to the console. And the tap>
function is in core, so no require needed.
I've found that I don't want to tap>
simple values, because single values do not provide much context into whatever problem I'm trying to solve. Instead I build a map that is tapped as a unit:
(defn fn-i-want-to-debug
[simple-arg sometimes-nil-but-too-large-to-print-arg]
(tap> {:in `fn-i-want-to-debug
:simple simple-arg
:nil? (nil? sometimes-nil-but-too-large-to-print-arg})
...)
Because of course in Clojure, it's always easy to just make a new map on the fly. Here, the backquote (it's the syntax quote) will ensure that the function name is a fully namespace qualified symbol.
As the above example shows, sometime an argument is too large to effectively print but maybe, for your debugging, you just need to know if it is nil or not ... or maybe you want to dissoc
some keys, or otherwise prepare the data that gets output. That's fine, that's why REPL oriented development can be faster and better than firing up the debugger.
A debugger can only show you one thing at a time; and even using Cursive, it can be a lot of clicks and a lot of squinting at the screen to see exactly what's going on.
By comparison, console output (via tap>
) can be a little history of what's going on in your program leading up to a problem.
Further, nothing is stopping you from using a debugger and tap>
together (but you may find that you don't need that nearly as often as you'd think).
tap>
is better than straight-up calls to prn
or pprint
because you can leave the calls to tap>
in your code with little or no cost; you can add-tap
when you need to ... and in fact, you can also remove-tap
when you want to continue developing without the extra output, all without restarting your REPL.
That's pretty classic Clojure for you ... a minimal and practical tool that becomes significantly more useful because Clojure and its REPL are so well wedded together.
Posted on September 15, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.