Misbalanced parens + hoisted defn = surprising runtime error in Clojure code

Today I ran into a frustrating syntax pitfall in Clojure, which I am attempting to learn using a simple homework assignment as motivation. Below is a simplified testcase containing the same mistake I had in my actual code. The code compiles, but at runtime I receive "java.lang.IllegalStateException: Var selfr is unbound.".

(defn magic []
  (println "magic happens") ; one too few closing parens

(defn selfr [i]
  (if (< i 0)
    0
    [(selfr (dec i))
     (selfr (dec i))]))) ; one too many closing parens

(defn selfr-caller []
  (selfr 3))

(defn -main [& args]
;  (magic) ; uncomment to make everything work
  (println (selfr-caller)))

If you stare at the code for a bit, you'll see that selfr is defined inside the magic function. So why doesn't the compiler complain that selfr-caller can't see it? What I didn't realize until today (with the help of some wonderful people on IRC) was that defn hoists the declaration to the top-level, where everything can see it... but the assignment won't occur until the code block is actually run.

This kind of bug is extraordinarily tricky to find, since it is ultimately a syntax error but may not show symptoms depending on the execution order of the code. I suppose this could have been prevented by having a better editor, such as one with draconian indentation support (e.g. Emacs) or perhaps a matching-paren highlighting feature.


Responses: 9 so far

  1. tzach says:

    Indentation is great, but in addition when using emacs/clojure I immediately evaluate each function as I'm writing it.
    At this point I do not even think about it.

  2. bhenry says:

    p-a-r-e-d-i-t!

  3. Psyllo says:

    Please don't take me (or yourself) too seriously when I say that's a pretty n00b mistake. I just want to give you some context to your bug.

    The culture of Lisp is that every Lisper hovers on the median of language user and language designer. Having power in your language is fundamental to Lisp.

    One concept that you brought up was that you don't necessarily feel that you should have to have help from your text editor to find this bug. Is response, the two most important things for reading and writing Lisp are indentation and paren matching. S-exps are so flexible and malleable that you're just torturing yourself needlessly by not having facilities in your text editor to wield them.

  4. Tim McCormack says:

    @Psyllo: Yes, it is absolutely a n00b mistake, and I wouldn't have blogged about it if it hadn't taken me an hour to figure out, and only then with the help of some friendly IRCers. (When I Googled for the exception string originally, there were no useful info. Now this post is the second result!)

    Unfortunately, I am horribly out of touch with emacs at this point, and have been using gEdit instead. Eclipse has a decent Clojure plugin, but I don't want a Development Environment, I just want a damn editor.

  5. Psyllo says:

    Awesome. You rawk. Thanks for doing that.

    As far as text editors go. I use Emacs with paredit. I'd recommend them to any geek not just carrier programmers. Especially for Lisp.

  6. Tim McCormack says:

    I'll get back into Emacs at some point, but for now I'm finding this to be a decent editor. It's completely alpha-stage software, but it has pretty much exactly what I want.

  7. Laurent Petit says:

    Hello,

    Of course, you could use Eclipse as a damn editor, if you could afford the slow startup time, and the fact that (gulp) there's currently a bug in ccw preventing it from opening a clj file from anywhere in the file system -soon to be corrected, though).

    My guess is that you would have spotted this almost immediately and visually with an editor providing not "paredit" (though this may work too, if you have matching paren highlighting), but "rainbow parens".

    So probably, vim + vimClojure, in your case.

    Cheers,

    --
    Laurent

  8. Laurent Petit says:

    empty comment, just to activate the follow up I did not activate in my first comment. :-/

  9. Tim McCormack says:

    Update: I'm using Emacs now. :-)