I mostly spent time today working on Chapter 8 and also Chapter 10. Chapter 8 turned out to be a bit of red herring with it's rocket modeling code. I was quite interested figuring out how it worked but for some reason, I wasn't able to replicate the results that the author got in his code with mine. I ended up spending some time trying to solve these issues but it felt counterproductive to focus on this rather than other learning topics. So I moved onto Chapter 10.
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 -
- Understand the Problem
- Devise a Plan
- Carry out The Plan
- Look Back
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.
- Isolate the problem
- Understand what the inputs and outputs are and see if your program handles unexpected inputs well
- Be careful when using mutable state since they can change unexpectedly. Try to printout the values of atoms and refs when you can.
- Writes tests that stress the boundaries of the program
- Use diagrams to get a visual understanding of the data flow
- Rethink the assumptions used to build the program
- Think about using an existing library to replace the broken code
- Try to get more diagnostics to explore where the program is breaking
- Ask for help on GitHub, SO, IRC etc.
- Mix experimentation with re-thinking the problem.
- Take a break :)
- Bounce ideas off of another human (rather than just muttering under your breath)
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
- 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.
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.
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.
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.