Learning clojure: debugging?

146 views
Skip to first unread message

Abraham Egnor

unread,
Jun 1, 2012, 2:18:41 PM6/1/12
to clo...@googlegroups.com
I'm early in the process of learning clojure, and am hoping that the community has suggestions for a frustration I've run into.

Preamble: as a learning exercise, I'm porting a hex-grid geometry library I originally wrote in Haskell.  Here's the relevant bit:

(ns hex.core)

(def origin {:x 0 :y 0 :z 0})

; Basic math operations
(defn add [point delta]
  {:x (+ (:x point) (:dx delta))
   :y (+ (:y point) (:dy delta))
   :z (+ (:z point) (:dz delta))})

(defn sub [point-1 point-2]
  {:dx (- (:x point-1) (:x point-2))
   :dy (- (:y point-1) (:y point-2))
   :dz (- (:z point-1) (:z point-2))})

(defn mul [delta scale]
  {:x (* (:dx delta) scale)
   :y (* (:dy delta) scale)
   :z (* (:dz delta) scale)})

; Distance between points is the sum of the two smaller coordinate deltas
(defn distance [point-1 point-2]
  (let [delta (sub point-1 point-2)
        dvals (sort (map #(Math/abs %) (vals delta)))]
    (+ (first dvals) (second dvals))))

(def directions #{:xy :xz :yz :yx :zx :zy})

(def delta {:xy {:dx 1  :dy -1 :dz 0}
            :xz {:dx 1  :dy 0  :dz -1}
            :yz {:dx 0  :dy 1  :dz -1}
            :yx {:dx -1 :dy 1  :dz 0}
            :zx {:dx -1 :dy 0  :dz 1}
            :zy {:dx 0  :dy -1 :dz 1}})

(defn neighbors [point]
  (map #(add point (delta %)) directions))

(defn line [point dist dir]
  (let [d (delta dir)]
    (map #(add point (mul d %)) (range 1 (inc dist)))))

Here's the problem - there's a bug in there.  Trying to call (line origin 1 :xy) generates a stack trace:

java.lang.NullPointerException: null
 at clojure.lang.Numbers.ops (Numbers.java:942)
    clojure.lang.Numbers.add (Numbers.java:126)
    hex.core$add.invoke (core.clj:8)
    hex.core$line$fn__1022.invoke (core.clj:42)
    clojure.core$map$fn__3811.invoke (core.clj:2430)
    clojure.lang.LazySeq.sval (LazySeq.java:42)
    clojure.lang.LazySeq.seq (LazySeq.java:60)
    clojure.lang.RT.seq (RT.java:466)
    clojure.core$seq.invoke (core.clj:133)
<lots more lines omitted>

I eventually tracked it down by evaluating each subexpression of line - the root bug is that mul should be returning a {:dx :dy :dz} map, not a {:x :y :z} one, so add gets nil for the subvalues, so in turn + raises the NPE.  Doing this manual trace was a significant interruption, though, and the stack trace isn't exactly a clear indication of what went wrong.

Is there some technique I'm not seeing to make this kind of simple typo-based error less of a hassle to track down?  Or is this simply a matter of getting better at deciphering the stack traces?

Sean Corfield

unread,
Jun 2, 2012, 5:13:11 PM6/2/12
to clo...@googlegroups.com
On Fri, Jun 1, 2012 at 11:18 AM, Abraham Egnor <abe....@gmail.com> wrote:
> I'm early in the process of learning clojure, and am hoping that the
> community has suggestions for a frustration I've run into.
...
> I eventually tracked it down by evaluating each subexpression of line - the
> root bug is that mul should be returning a {:dx :dy :dz} map, not a {:x :y
> :z} one, so add gets nil for the subvalues, so in turn + raises the NPE.
...
> Is there some technique I'm not seeing to make this kind of simple
> typo-based error less of a hassle to track down?  Or is this simply a matter
> of getting better at deciphering the stack traces?

I'm curious about your process for creating the solution...

Did you evolve it piece by piece in the REPL? Did you write tests for
each operation and then evolve the code until they passed? Did you
start at the top of the file and just write code until you got to the
bottom and then test the whole thing?

(obviously loaded questions - my follow-up would be that if you
started in the REPL or via a TDD-style approach, you'd have probably
caught the error earlier - but, yes, Clojure stack traces can be
pretty daunting at first)

Looking at the stack trace, the NPE comes from hex/core.clj line 8,
invoked from hex/core.clj line 42 inside the line function
(hex.core$line), during evaluation of an anonymous function (the
$fn__1022 part). That would narrow it down quite a bit and you could,
in the REPL, try (mul (delta :xy) 1) and see what you get, then (add
origin *1) ;; *1 is bound to the last result, (mul (delta :xy) 1) in
this case -- and you'd see your NPE and it should be easy to see
why... Which is why a REPL-first or test-first process would have hit
it as soon as you composed mul and add, or possibly even when you just
saw mul evaluated?

HTH... Welcome to Clojure, BTW!
--
Sean A Corfield -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
World Singles, LLC. -- http://worldsingles.com/

"Perfection is the enemy of the good."
-- Gustave Flaubert, French realist novelist (1821-1880)

Moritz Ulrich

unread,
Jun 2, 2012, 5:34:10 PM6/2/12
to clo...@googlegroups.com
On Fri, Jun 1, 2012 at 8:18 PM, Abraham Egnor <abe....@gmail.com> wrote:
> Is there some technique I'm not seeing to make this kind of simple
> typo-based error less of a hassle to track down?  Or is this simply a matter
> of getting better at deciphering the stack traces?

I think one important point here is that you use two different data
structures to hold the same kind of data. Why use a map of #{:dx :dy
:dz} and a map of #{:x :y :z} for the same use? Why not represent
deltas using :x :y and :z too?
And if we're on this track, why a map? Will the points contain other
important data? If not, I'd just use a vector of n items, representing
n dimensions. This would simplify the code and make it much easier to
maintain. All functions could operate on generic vectors describing
any point in n-dimensional space :-)

Regarding the debugging statement:

If you're using emacs and Slime, there's a full-blown debugger
integrated in swank-clojure. It features breakpoints, watches, etc.
though I rarely use it.
When I encounter such problems, I usually just throw in one or two
println statements printing the parameters. This way it's easy to
check if wrong values are passed.
Adding some asserts is helpful too: In your case it would be wise to
check at the start of the `add' function if `delta' really has the
keys #{:dx :dy :dz}.

--
Moritz Ulrich

Sean Corfield

unread,
Jun 2, 2012, 5:40:02 PM6/2/12
to clo...@googlegroups.com
On Sat, Jun 2, 2012 at 2:34 PM, Moritz Ulrich
<ulrich...@googlemail.com> wrote:
> I think one important point here is that you use two different data
> structures to hold the same kind of data.

Points and deltas are not the "same kind of data". Yes, they both have
x/y/z values but their meaning is different. Perhaps {:point [x y z]}
and {:delta [x y z]} might be a better choice (combining the vector
approach you suggest while still distinguishing the 'types' that the
OP wants)?

> If you're using emacs and Slime, there's a full-blown debugger
> integrated in swank-clojure. It features breakpoints, watches, etc.

True, and it's very powerful.

> When I encounter such problems, I usually just throw in one or two
> println statements printing the parameters. This way it's easy to
> check if wrong values are passed.

Perhaps clojure.tools.trace would be easier?

https://github.com/clojure/tools.trace

(I keep meaning to switch to using this instead of just adding println
statements!)

> Adding some asserts is helpful too: In your case it would be wise to
> check at the start of the `add' function if `delta' really has the
> keys #{:dx :dy :dz}.

Yes, :pre / :post would be another useful technique here! Good suggestion!

Softaddicts

unread,
Jun 2, 2012, 6:26:54 PM6/2/12
to clo...@googlegroups.com
clojure.tools.trace beats println by far (biased advice, I maintain it....:)))
It's also easier to segregate between debug and normal output in the code.

You can enable/disable fn tracing dynamically from the REPL for all fns in a given
namespace.

I seldom use a debugger. When I do it's to dive in the clojure runtime.

The REPL and trace tool meet my needs most of the time.

The trick is to avoid having huge chunks of code stuffed
in a single fn. It makes life harder. No dumb rule of thumb here (have no more
than xx lines per fn, blabla, ...).

Just make sure you have testable fns of reasonnable scope.

With the trace output, you can then isolate the culprit and test it standalone with
its input arguments captured from the trace (cut & paste).

Luc P
> --
> You received this message because you are subscribed to the Google
> Groups "Clojure" group.
> To post to this group, send email to clo...@googlegroups.com
> Note that posts from new members are moderated - please be patient with your first post.
> To unsubscribe from this group, send email to
> clojure+u...@googlegroups.com
> For more options, visit this group at
> http://groups.google.com/group/clojure?hl=en
>
--
Softaddicts<lprefo...@softaddicts.ca> sent by ibisMail from my ipad!

Vinzent

unread,
Jun 2, 2012, 8:22:44 PM6/2/12
to clo...@googlegroups.com
BTW, you may want to use clojure-contracts (https://github.com/dnaumov/clojure-contracts) instead of asserts or :pre\:post in order to get much nicer and informative error reporting.

Sean Corfield

unread,
Jun 2, 2012, 8:31:50 PM6/2/12
to clo...@googlegroups.com
Or keep an eye on https://github.com/clojure/core.contracts currently
available as [org.clojure/core.contracts "0.0.1-SNAPSHOT"]

Vinzent

unread,
Jun 3, 2012, 4:55:32 AM6/3/12
to clo...@googlegroups.com
Actually, it's kinda the same (Fogus and me decided to merge trammel and clojure-contracts into one library)

воскресенье, 3 июня 2012 г., 6:31:50 UTC+6 пользователь Sean Corfield написал:

Sean Corfield

unread,
Jun 3, 2012, 3:14:45 PM6/3/12
to clo...@googlegroups.com
On Sun, Jun 3, 2012 at 1:55 AM, Vinzent <ru.vi...@gmail.com> wrote:
> Actually, it's kinda the same (Fogus and me decided to merge trammel and
> clojure-contracts into one library)

Yeah, I figured. I just wanted to point people to the newly created
contrib library since that's where (I assume) future development will
occur.

Sean Neilan

unread,
Jun 3, 2012, 3:28:03 PM6/3/12
to clo...@googlegroups.com
Does Clojurescript have a trace function?

Sean Neilan

unread,
Jun 3, 2012, 3:49:07 PM6/3/12
to clo...@googlegroups.com
Nvm. Not yet.

I'm reluctant to dive into clojurescript because the debugger and trace functions aren't ready yet. 

I suppose if I make test cases for everything and stick to tiny functions, I should be alright.

Anyway, if Chris Granger uses it, it's probably pretty good.

HERE GOES!

Softaddicts

unread,
Jun 3, 2012, 4:59:07 PM6/3/12
to clo...@googlegroups.com
Not yet, I'll put this on my agenda. I need some research time not being
familiar yet with how it would translate in ClojureScript and if it's worthwhile
to implement it.

Comments from any one using ClojureScript ?

Luc
Reply all
Reply to author
Forward
0 new messages