DEV Community 👩‍💻👨‍💻

Cover image for Debugging Clojure at the REPL using tap>
Howard M. Lewis Ship
Howard M. Lewis Ship

Posted on • Updated on

Debugging Clojure at the REPL using tap>

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})
   ...)
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
marciol profile image
Marcio Lopes de Faria

Nice @hlship

I wrote about debugging experience with Elixir recently, because I miss the set of REPL tools that Clojure provides, as tap>. I think that it worth to notice because I know that you are familiar with Elixir as well.

Well done!

Collapse
marciol profile image
Marcio Lopes de Faria

Ah, and talking about it, this is the post: dev.to/marciol/inspector-a-better-...

🤔 Did you know?

 
🌚 Dark mode is available in Settings.