The Power of Clojure: Debugging
A common question we hear is "How do I use Clojure for real?" Not the language basics, but the practicalities of building software – questions like how to structure the project file tree and namespace hierarchy, how to write tests, or how to use the REPL.
This post tackles a small subset of this: debugging, and more specifically, REPL-based debugging in Clojure.
Debugging is fundamentally difficult. Clojure is a simple language, which helps with the cognitive load of debugging, and the debugging tools available to Clojure programmers are simple and powerful too.
Debugging in Clojure with a REPL involves using a number of small, simple tools in a systematic approach to achieve a thorough understanding of potentially complex situations. In this post, we will cover both these tools and the approach with which to apply to the tools to solve your debugging problems.
Why Is This Important?
A few years ago, a Cambridge University study found that "[...] on average, software developers spend 50% of their programming time finding and fixing bugs." Debugging is inevitable, necessary, important work.
For our purposes, we will take a look at two aspects of debugging: exploration of existing code and fixing bugs.
Exploration: When exploring new projects, debugging is often how you start learning about how the code works and how it's structured. When you need to alter code structure (e.g. breaking apart a large function before changing its behavior) you need to understand how it works. Learning how libraries work can be difficult. Often the documentation is insufficient and it’s necessary to dive in to the code to answer questions.
Fixing Bugs: It's unavoidable, it will be necessary to solve issues that present themselves in production. People will always write bugs, therefore you will always have to debug code.
Both of these contexts are an intrinsic part of the work of a software developer, and if these constitute 50% of your work (at least in time), debugging skills are important to hone. But note: debugging skills are just that: skills. Learnable, systematized, something to study.
What We Won’t Cover:
Whilst important and and related, this post won’t cover the following:
Setting up logging frameworks
Reading and understanding Clojure/Script exceptions
Profiling (understanding the performance characteristics of code)
Interactive debuggers such as CIDER’s debugger or Cursive’s integration with IntelliJ’s debugger.
Debugging Framework: The Scientific Method
Debugging with the scientific method is hardly a novel idea and plenty of great resources exist, but if you’re looking for some information specific to Clojure, check out Stu Halloway’s 2015 Conj talk (video, slides and links) as well as Kyle Kingsbury’s "Clojure from the Ground Up" chapter on debugging. Kyle’s work specifically covers debugging and many other valuable techniques.
- Observe the situation (problem)
- Form a hypothesis to explain the problem
- Devise and execute a test to show the correctness of your hypothesis
- Inconclusive? Try again.
- Conclusive? Good! The problem space has been reduced, try again with a smaller subset.
Contextually, we use debugging techniques to help observe and understand a problem, and to quantitatively evaluate the outcome of a test.
Instrumenting: Inspecting, Tracing
Adding instrumentation to code is like adding gauges and meters to a mechanical system.
Instrumentation can be added to code in two ways: by changing the code to explicitly instrument a particular value or control path, or by changing the runtime environment to provide insights into the code running within.
Changing the code is easy, precise, but often messy, overly redundant, and prone to leaving the instrumentation in by mistake.
Instrumenting the runtime is powerful and more easily able to paint a fuller picture, but it is generally more complex and harder to master, and can overwhelm you with data.
The code samples used below are variations examples of a typical programming problems. The basic overview with lots of annotations and comments can be found here:
Clojure Debugging Playbook
The three following sections outline techniques for:
- instrumenting the code,
- Instrumenting the runtime, and
- using the REPL itself as a debugging tool.
In our experience, problems are usually solved by applying a combination of two or three of these techniques.
Instrumenting the Code
Clojure is a dynamic language. It’s very easy to make changes to a function, eval the code in the REPL, and immediately observe the effects. We can use this tight feedback loop to directly instrument the code.
"The most effective debugging tool is still careful thought, coupled with judiciously placed print statements." – Brian Kernighan, Unix for Beginners (1979).
First in our list is probably every programmer’s first debugging technique: println. It’s sometimes seen as a naive approach, perhaps because in compiled languages, like C or Java, the edit/execute/inspect cycle time can be so long. However, in an immutable, functional, dynamic language such as Clojure it is highly effective.
The technique is simple: to see value of an s-expression as the code is executed, println it. To trace the execution of the code, add println statements to act as a “breadcrumb trail” through the code.
For example, the example code has an infinite loop. Knowing that recursion can be tricky, we add a little println into the loop to see what’s happening:
We never finish processing the first account. Looking at the loop/recur, we never reduce the size of the accounts collection - we forgot a (rest accounts) in the recur call.
When you have more than a couple of print statements you will need to be careful to provide enough context in your println so that you know what each output value corresponds to. You might also have to change the structure of your code a little, e.g. introducing a let binding to capture a variable’s value. You can make this easier with a short helper function:
When using printlns it’s necessary to delete them prior to committing code. It’s very easy to forget and leave them in. This problem can be mitigated by using a logging framework and adjusting environments to output the logs at appropriate levels. We cover more on logging below.
inspecting let bindings
We often want to see one or more of the values in a let binding. A specific instance of println value inspection is to use the _ (underscore) convention for an unused variable to act as the name to bind in order to add a println call directly into the let bindings.
Looking at the full stacktrace from the last error, we can see the error is thrown from inside the as-currency function. We can introduce a let and inspect all the intermediate values to see where the problem might be:
A common error is forgetting the _ binding before each println.
A more elegant way to do this is with a debugging let macro. This example shows how it works, but you would likely put the dlet definition in your user namespace somewhere.
The macro instruments every binding in the let form automatically, printing out all data with a single additional character. This is a useful timesaver for larger let forms.
Inspecting threading intermediates
Again, another special case of println debugging is to inspect the intermediate values in a chain of threaded functions.
This technique inserts an invocation of an identity function into a list of threaded forms, but the function has the additional side effect of printing out its argument. The following example shows both the direct inline anonymous function, and a quick helper function:
The double parens aren’t very common, but here they are necessary - we use them to define and immediately execute a function. Also, we use them to make sure to return the value from the inspection function!
Logging: it's basically println debugging but better. From a debugging point of view it is useful because you get a file name and line number for each log statement. From a maintenance point of view, you can leave the log statements in but disable them from printing via configuration.
Each log statement is assigned a log level (typically trace, debug, info, warning, and error) and the log framework is configured to emit log statements at a particular log level on a per-namespace basis. For example, you may want to configure your development environment to print all debug statements, however you just want warnings and errors in production.
Like println, logger calls return nil, so if you are instrumenting a function that needs a return variable, you may need to capture the value to log and then return it. Here are a couple of ways to do that:
The Spyscope library is like a ninja version of println debugging. From its GitHub page:
Spyscope includes 3 reader tools for debugging your Clojure code, which are exposed as reader tags: #spy/p, #spy/d, and #spy/t, which stand for print, details, and trace, respectively. Reader tags were chosen because they allow one to use Spyscope by only writing 6 characters, and since they exist only to the left of the form one wants to debug, they require the fewest possible keystrokes, optimizing for developer happiness.
#spy/p has the same uses and drawbacks of println debugging, but it’s shorter and easier to add into code, and it outputs pretty printed colorized output.
#spy/d is even more powerful, showing one or more stack frames for where it was called from, timing data, and lots of other useful stuff. The project README contains full documentation of all spyscope’s functionality.
Here is a screencap of how you can quickly add spyscope tracing to your code, and what the output looks like:
Instrumenting the runtime
There are a number of more sophisticated tools that observe control flow and return values on your behalf, giving you ways to view and even interact with that data.
clojure/tools.trace (+ CIDER)
The canonical tracing library, clojure.tools.trace, exposes two functions to add traces and two functions to remove them, one each for functions and for namespaces. Tracing is applied dynamically without modifying the code being instrumented. As traced functions are run, arguments and return values are printed. Nested function calls are printed with nesting indicated.
This screencap shows the same faulty bank transactions example, first being run without tracing, tracing getting applied, and then running with tracing:
Helpfully (for debugging), the function throwing an exception is printed last, clearly showing its arguments.
CIDER has a couple of convenience functions to help with tracing and untracing, plus highlights traced functions.
Clearly, these are powerful tools. You get to see argument and return values, and get a sense of the control flow, with no need to modify the source itself. A possible downside is that you can get an overwhelming amount of trace data back.
An additional noteworthy mention is for Sayid. It is an inspecting debugger for the Clojure runtime and has a corresponding mode for Emacs. It offers even more advanced views into the data with the ability to drill down and even replay execution.
One final advantage of inspecting the runtime instead of modifying the code is it is a safer technique: if you have to connect to a production server’s nREPL to live debug a particularly tricky problem, adding traces to the vars is much less error prone than adding instrumentation to the code and re-evaluating live.
re-frisk / re-frame-trace
Re-frisk and re-frame-trace are specific to ClojureScript re-frame apps, but, given their current popularity, these tools are worth mentioning. Both provide inspection tools for the global application state, and an inspectable log containing the sequence of re-frame events triggered during the application’s use. In combination with the other debugging techniques shown here, this specialized view of the re-frame events is very useful for solving the front end bugs that are typically hard to track down.
The least obvious way to debug for new Clojure programmers is to leverage the language’s dynamic nature and tight REPL integrations. Of the three general categories of debugging tools presented here, this is the most powerful and the most general purpose. It is also a key technique for effectively authoring Clojure code.
Using the REPL to eval code
Fast keystroke-based REPL integration with your editor is a must. Emacs, Atom, Cursive and Vim all support this workflow. We highly recommend you become familiar with this way of working. Is truly one of the most amazing aspects of programming in Clojure.
You can use your editor to evaluate code in the Clojure REPL directly from the source code. You might be evaluating a form to see what it evaluates as, or to observe any side effects (e.g. println output). Or you might be evaluating a new function, or a newly changed function, so that it is available for use by other code. Notably with all of these, you don’t need to reload your project, restart the REPL, type directly into the REPL, or even save your source file. Your Clojure runtime is always running; you can change the program, execute parts of it, or write new parts and see how they work in context.
Perhaps the best way to explain this is by example: using the same code sample from earlier, I run the calculation and see an exception, add some instrumentation to observe the program flow, spot a problem case and run that manually, fix the code error and load the fixed function, then re-run the code. The pane on the right shows the Emacs keystrokes and more importantly the commands they correspond to.
repl, def, eval
Many times you want to evaluate how some particular function might be breaking. We’ve shown a number of ways how you can insert code to instrument a function’s operation, but you can use the REPL instead.
Stu Halloway has another fantastic blog post where he uses a combination of binary search, brainpower, and the REPL to debug an error without even having access to a stacktrace. The REPL technique he uses is this: temporarily define variables that correspond to the names used within your function, then evaluate the code in your function directly.
For example, say we came up with an as-currency function that would handle messier input, like comma separators for thousands:
Running the function throws an exception, so something isn’t right. The following screencap shows how we test how the components of the bigdec conversion work assuming the negative? and cleaned-amount checks are correct, then narrowing the problem to the value of cleaned-amount. That’s definitely not as expected, and so we further narrow the problem to the str/replace call:
Capture runtime data for post-run REPL analysis
In the previous example, by manually entering some appropriate data and binding a var we can evaluate subcomponents of a function independently. Sometimes the data we need to correctly operate a function is more complex than can reasonably be typed in. Similarly, it might be that you don’t quite know the exact data to “break” a function, but you can run the code from its top level and get the code to break.
In these cases, we can capture runtime data, either by defining a var or by using an atom. Then, when the main execution flow has completed, we can use the data we captured and evaluate that data using our functions, under controlled conditions in the REPL.
In wrapping up, another of Brian Kernighan’s brilliant aphorisms is applicable:
"Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?" – Brian Kernighan, The Elements of Programming Style
While this has been an entire article focused on inspecting and instrumenting code at run time, without an understanding of the code at hand these efforts are seriously hampered. We must always be able to read and comprehend the code that we are evaluating. When you write code, always strive for readability. Your 6-months-future self will thank you.
Hopefully we’ve helped answer the question, “How do I debug in Clojure?”
Remember these three general techniques:
- instrument the code,
- instrument the runtime, and
- use the REPL itself as a debugging tool.
You can modify code live in a REPL or editor (depending upon your build setup) to quickly add println, log, and trace statements. You can temporarily define variables and atoms to capture and inspect values in a connected REPL, or inspect a re-frame SPA’s app-db and all events in the system from a browser window with tools like re-frame-trace and re-frisk. There are myriad techniques to help you understand what your Clojure and ClojureScript code is doing, and in practice you will probably use more than one of these at the same time
Once you are familiar with these techniques, you will learn which is the most effective to use to solve a particular problem. The techniques all overlap, each having its own particular pros and cons, and each having different maintainability, complexity, and flexibility characteristics.
Remember though, the easiest program to debug is the one that doesn’t need it, because it is written so clearly that any errors are obvious. Decomposing code into simple, pure functions, covered by reasonable unit tests, goes a long way towards not needing to use the above techniques too often.