Overthunk

Debugging Strategies

Jul 1, 2020


Contents

What I learned Today

After moving on from Chapter 8, Chapter 10 seemed interesting since it delved into practical ways of dealing with errors and how to debug them. The author mentions some steps to debugging problems (borrowed from a very excellent book - How to Solve It)

The Debugging mindset -

This 4 step process can be attempted recursively to fine-tune solutions to the problem.

There are also quite a few pieces of advice that is laid out in Ch 10 that will be useful to anyone programming in general.

Debugging Clojure with Stacktraces

We have a function to bake a given cake:

(defn bake
  "Bakes a case for a certain amt of time. Returns a cake with new :tastiness level"
  [pie temp time]
  (assoc pie :tastiness
         (condp (* temp time) <
           400 :burned
           350 :perfect
           300 :soggy)))

Now, calling (bake {:flavor :rhubarb} 375 10.25) gives us this error

  1. Unhandled java.lang.ClassCastException class java.lang.Double cannot be cast to class clojure.lang.IFn (java.lang.Double is in module java.base of loader ‘bootstrap’; clojure.lang.IFn is in unnamed module of loader ‘app’)

Let’s get the full stacktrace using the pst function.

ClassCastException class java.lang.Double cannot be cast to class clojure.lang.IFn (java.lang.Double is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')
ground-up.debugging/bake (debugging.clj:8)
ground-up.debugging/bake (debugging.clj:4)
ground-up.core/eval7794 (form-init4440364485622416696.clj:25)
ground-up.core/eval7794 (form-init4440364485622416696.clj:25)
clojure.lang.Compiler.eval (Compiler.java:7177)
clojure.lang.Compiler.eval (Compiler.java:7132)
clojure.core/eval (core.clj:3214)
clojure.core/eval (core.clj:3210)
nrepl.middleware.interruptible-eval/evaluate/fn--935 (interruptible_eval.clj:91)
clojure.main/repl/read-eval-print--9086/fn--9089 (main.clj:437)
clojure.main/repl/read-eval-print--9086 (main.clj:437)
clojure.main/repl/fn--9095 (main.clj:458)

That’s a lot of info. The main exception seems to be the ClassCastException where java.lang.Double cannot be cast to clojure.lang.IFn. Someone learning Clojure has probably seen this particular error dozens of times.

In this case, the stack strace points us to line 8 in debugging.clj. That line is our (condp (* temp time) < expression. clojure.lang.IFn is a public interface that provides complete access to invoking any of Clojure’s APIs. Looking at the documentation for it tells us that this is probably how we’re able to call/invoke functions in general in Clojure.

Now, the error tells us that a Double cannot be cast to IFn. That makes sense since a Double is a data type holding numeric values and as such doesn’t have function like properties.

Hmmm. Now that we have some more information about the error message, let’s try to understand why we’re getting it. The error seems to be in the condp statement.

condp takes a pred, an expression and a set of clauses to test the predicate against. Our predicate seems to be (* temp time). Oh. For each of the clauses present in the condp, the test would be pred expr test-expr. In our case, this is (* 375 10.25) < 400) which evaluates to (3843.75 < 400). Clearly, this is invalid Clojure syntax since the first verb should be a function, not a number.

Fortunately this error is easy to fix. We just swap the pred and the expr.

Takeaways

There are a few more errors that Chapter 10 delves into but these are a little harder to debug due to the nature of how the JVM represents the stack trace. We also look at a NullPointerException and how to fix it by walking through and printing each value in a sequence to see why it’s generating nil.

After working with Rust for a while, I realized that I’ve been spoiled by Static Type systems and extremely clear and helpful compiler error messages. In Clojure, we gain a lot of flexibilty and elegant constructs but at the cost of the safety that comes with static typing.

Some of these pain points can be mitigated by having a robust test suite that watches for these kinds of errors and offloads the mental effort onto the tests. Hopefully it also becomes easier to understand the error messages after just having worked with the language for enough time.

Athens Issues

On a different note, my fellow Team Seneca members and I have decided on an issue to fix in the Athen’s codebase. Curretly, Athena, the search widget has some general issues on non-chromium based browsers. This seems like relatively important problem that will have a lot of upside. We’d also get a lot of value from learning the codebase and how all the different technologies like re-frame, Datascript and posh work.

Next Steps

This marks the end of Clojure from the Ground Up! This was really a good beginner resource and I learned so many facets of Clojure and programming strategies in general. I’m sure I will be referring to it as I progress with learning Clojure. At this point, I’m seriously considering what direction to take in my ClojureFam journey. After having finished Clojure from the Ground Up, I’m unsure if I should focus my efforts on seriously trying to contribute to the Athen’s codebase or if I should keep working on learning Clojure concepts with Brave Clojure.

Maybe I should try doing a bit of both for now and see where that takes me.