Clojure Reader Macros

Unlike Common Lisp, Clojure doesn't support user-defined reader macros. You can read some of the rationale for why in this chat log, among other places. I think that's probably a good decision; I don't see a lot of need for mangling the reader. Regular macros get you pretty far already and Clojure has built-in reader support for all the good stuff.

But how hard would it be to have custom reader macros in Clojure if you wanted them? Turns out not too hard if you're willing to ruthlessly break encapsulation and rely on implementation details. Here's one way you could define a dispatch reader macro (i.e. one starting with # and some specified second character):

(defn dispatch-reader-macro [ch fun]
  (let [dm (.get (doto (.getDeclaredField clojure.lang.LispReader "dispatchMacros")
                   (.setAccessible true))
                 nil)]
    (aset dm (int ch) fun)))

Pass in a character and an fn and you get a reader macro. For a silly example let's make reader syntax to uppercase a literal string.

(defn uppercase-string [rdr letter-u]
  (let [c (.read rdr)]
    (if (= c (int \"))
      (.toUpperCase (.invoke
                     (clojure.lang.LispReader$StringReader.)
                     rdr
                     c))
      (throw (Exception. (str "Reader barfed on " (char c)))))))

The function is passed a reader and the dispatch character (which you can usually ignore). I cheat and use Clojure's StringReader to do the real work.

Now I can do this:

user> (dispatch-reader-macro \U uppercase-string)
#<user$uppercase_string__1295 user$uppercase_string__1295@9b59a2>

user> #U"Foo bar BAZ"
"FOO BAR BAZ"

user> (println #U"foo\nbar")
FOO
BAR
nil

user> #U(blarg)
java.lang.Exception: Reader barfed on (

(= "FOO" "foo")
false

(= "FOO" #U"foo")
true

Oh sweet Jesus don't use this in real code, because:

  1. The community will rightly hunt you down with torches and pitchforks.
  2. Reader macro characters are reserved and may conflict with later changes to the core language.
  3. These are set globally, not per-namespace.
  4. And so on. Just don't.

But I think it's a nice demonstration. I've read opinions that Clojure isn't a Real Lisp™ because a lot of Clojure is written in Java and isn't extensible in Clojure itself, but that's generally not true. The reader code for Clojure was all written in Java, but above I modify it from Clojure. There is no line separating Java-land and Clojure-land. It's all one big happy family.

April 18, 2009 @ 10:14 AM PDT
Cateogory: Programming
Tags: Lisp, Clojure

3 Comments

rodjure
Quoth rodjure on October 29, 2009 @ 12:08 PM PDT

Doesn't work any more with clojure 1.1 alpha:

Clojure 1.1.0-alpha-SNAPSHOT user=> (defn dispatch-reader-macro [ch fun] (let [dm (.get (.getDeclaredField clojure.lang.LispReader "dispatchMacros") ni l)] (aset dm (int ch) fun)))

'user/dispatch-reader-macro

user=> (defn uppercase-string [rdr letter-u] (let [c (.read rdr)] (if (= c (int \")) (.toUpperCase (.invoke (clojure.lang.LispReader$StringReader.) rdr c)) (throw (Exception. (str "Reader barfed on " (char c)))))))

'user/uppercase-string

user=> (dispatch-reader-macro \U uppercase-string) java.lang.IllegalAccessException: Class user$dispatch_reader_macro__463 can not access a member of class clojure.lang.LispReader with modifiers "static" (NO_SOU RCE_FILE:0)

rodjure
Quoth rodjure on October 29, 2009 @ 12:40 PM PDT

worked with this slight mod, could just be a glitch in the alpha version.

(defn dispatch-reader-macro [ch fun] (def a (.getDeclaredField clojure.lang.LispReader "dispatchMacros") ) (let [dm (.get a nil)] (aset dm (int ch) fun)))

Brian
Quoth Brian on December 16, 2009 @ 10:55 AM PST

I updated the code a bit so it works now.

Speak your Mind

You can use Markdown in your comment.
Email/URL are optional. Email is only used for Gravatar.

Preview