I find it very difficult to debug the Clojure errors I have in my code compared to all the other programming languages I've used. My primary programming language is Java and I'm very new to Clojure. The majority of my time writing Clojure is spent trying to figure out, "Why am I getting this error?" and I'd like to change that. I'm using CounterClockWise as my primary IDE. I don't know how to use Emacs (yet?).
Here's an example:
(ns cljsandbox.core)
(def l [1 2 3 1])
(defn foo
[l]
(->> l
(group-by identity)
;vals ;commented out to show my intent
(map #(reduce + %))))
Here, I mistakenly thought that group-by
returns a list of lists, but it actually returns a map of <key, list<value>>
or however you'd phrase it in Java terms. This gives an error message that says:
ClassCastException clojure.lang.PersistentVector cannot be cast to java.lang.Number clojure.lang.Numbers.add (Numbers.java:126)
This isn't very helpful because there isn't a stack trace. If I type (e)
it says:
java.lang.ClassCastException: clojure.lang.PersistentVector cannot be cast to java.lang.Number
at clojure.lang.Numbers.add (Numbers.java:126)
clojure.core$_PLUS_.invoke (core.clj:944)
clojure.core.protocols/fn (protocols.clj:69)
clojure.core.protocols$fn__5979$G__5974__5992.invoke (protocols.clj:13)
clojure.core$reduce.invoke (core.clj:6175)
cljsandbox.core$foo$fn__1599.invoke (core.clj:10)
clojure.core$map$fn__4207.invoke (core.clj:2487)
clojure.lang.LazySeq.sval (LazySeq.java:42)
I have no idea how I can go from this error message to understanding, "You thought you were passing in a list of lists into map
but you were really passing in a map datatype". The stack trace shows the problem was reported inside of reduce
, not inside of group-by
, but IMO, that is not where I as a human made my mistake. That's just where the program discovered a mistake had been made.
Issues like these can take me 15+ minutes to resolve. How can I make this take less time?
I know it's too much to expect a dynamic language to catch these errors. But, I feel like the error messages of other dynamic languages like javascript are much more helpful.
I'm getting pretty desperate here, because I've been coding in clojure for 1-2 months now and I feel like I should have a better handle on figuring out these problems. I tried using :pre
/:post
on functions but that has some problems
- The reporting on
:pre
/:post
kind of sucks. It only prints out literally what you test. So unless you put a lot of effort into it, the error message isn't helpful. - This doesn't feel very idiomatic. The only code I've seen that uses
:pre
/:post
are articles that explain how to use:pre
/:post
. - It's a real pain to pull out the steps of a threading macro into their own
defn
s so that I can put the:pre
/:post
in them. - If I followed this practice religiously, I think my code could become as verbose as Java. I'd be reinventing the type system by hand.
I've gotten to the point where I pepper my code with safety checks like this:
(when (= next-url url)
(throw (IllegalStateException. (str "The next url and the current url are the same " url))))
(when-not (every? map? posts-list)
(throw (IllegalStateException. "parsed-html->posts must return a list of {:post post :source-url source-url}")))
Which only fixes that first bullet point.
I feel like either
- I've got a development process that's very, very wrong and I don't know it
- There's some debugging tool/library out there that I don't know about that everyone else does
- Everyone else is having problems like this and it's Clojure's dirty little secret / Everyone else is used to dynamic languages and expects to go through the same effort I am going through to resolve errors
- CounterClockWise has some bug that's making my life way harder than it needs to be
- I'm supposed to be writing a lot more unit tests for my Clojure code than I do for my Java code. Even if I'm writing throwaway code.