Eli Bendersky recently blogged about debugging Clojure code. His post is worth reading, and shows how to read stacktraces and add instrumentation for tracing. In this blog post I am going to debug his example program without using such tooling, relying only on the rapid feedback loop provided by the REPL.
Here's the problem program:
(defn foo [n] (cond (> n 40) (+ n 20) (> n 20) (- (first n) 20) :else 0))
Before I even show you this program failing, I am going to deliberately obfuscate Clojure's error reporting. I am going to throw away all the potentially useful information: the exception classes, the stacktraces, and the error messages. To do this I will start a subrepl that snidely elides all information about exceptions caught by the REPL:
(require '[clojure.main :as main]) (main/repl :caught (fn [_] (println "Broken! HaHa!")))
Now we are ready to see the problem:
(foo 24) => Broken! HaHa!
foo is broken for the value
24. We will look at three ways to track this down:
To blindly subdivide the problem, start with the your problem case and explicitly eval all the subpieces. This will often identify which subpiece is the problem, and you can work your way down until you have a problem that you can understand at a glance. Since
foo uses only one name,
n, I will temporarily def that name at the top level:
(def n 24)
Now I can evaluate every subform in foo, just by putting my cursor at the end of each form and sending that form to my REPL (all Clojure editors support this). The definition of
foo has only one form in its body, the
cond, so I will eval that first:
(defn foo [n] (cond (> n 40) (+ n 20) (> n 20) (- (first n) 20) :else 0)◼︎) => Broken! HaHa!
The ◼︎ character indicates the position of the cursor when I ask my REPL to evaluate the previous form. Since the
cond also appears broken, I will now evaluate all six of the
cond's subforms in quick succession:
(defn foo [n] (cond (> n 40)◼︎ (+ n 20)◼︎ (> n 20)◼︎ (- (first n) 20)◼︎ :else◼︎ 0◼︎)) ;; results shown in order the ◼︎ appears: (1) => false (2) => 44 (3) => true (4) => Broken! HaHa! (5) => :else (6) => 0
Now we are getting somewhere! Of the six subforms of
foo, only the fourth one,
(- (first n) 20), causes the problem. Subdividing one last time:
(defn foo [n] (cond (> n 40) (+ n 20) (> n 20) (-◼︎ (first n)◼︎ 20◼︎) :else 0)) ;; results shown in order the ◼︎ appears: (1) => #object[...] (2) => Broken! HaHa! (3) => 20
At this point it is pretty clear that calling
first on a number is not valid. A quick
doc check will show that
first is for collections:
(doc first) ------------------------- clojure.core/first ([coll]) Returns the first item in the collection. Calls seq on its argument. If coll is nil, returns nil.
If this approach seems dumb to you, good! It is dumb by design, and can mindlessly find things that a mind might space out and miss.
Now let's solve the problem again, smarter:
To the extent that you are familiar with a language, you may be able to execute code in your head even faster than you can navigate forms in an editor. I know Clojure pretty well, so my internal monologue looking at this code would be something like:
foois being passed one arg, the integer
24will take me down the second branch
firston a number!
For me and for this problem, there is a good chance that this approach would be faster than blindly subdividing. But this is situational, and we are all fallible, so how about mixing these two approaches?
The Goldilocks rule tells us that if blindly subdividing the problem is too dumb, and executing the code in your head is too clever, then you should aim for a happy medium of the two. This is in fact my normal operating mode when solving function-level problems in Clojure. How do you know if you are at a happy medium? If you execute the code in your head and don't get the same answer the REPL is giving you, dumb it down a little and subdivide the problem.
It is worth noting that this entire discussion can also be applied to writing code, not just to debugging. When you write code, you are imagining what needs to happen (executing not-yet-written code in your head), and breaking that into a set of steps (subdividing the problem). If, while you are developing, you try each subform at the REPL, you are far less likely to make the kind of mistake we are examining here. Also, this style catches a certain category of errors more quickly than you can catch them with unit testing or TDD.
Clojure's tangible environment (the REPL, namespaces, vars, etc.) make it possible to inspect and modify your program directly, quickly adjusting your focus as you are solving problems. Of course there are many other tools you might use for debugging:
All of these tools can and should be made better for Clojure. But these other tools are all generic, and are likely available in any language. The next time you are debugging a problem, and before you opt for a generic experience, think about what makes Clojure special and what you could do from the REPL.