Monday 6 August 2012

Macros I've been using

Here are a few macros I've been using.

The epic trio of Debug macros:

 

1. The regular, simple debug:

(defmacro ?? [& form]
  `(let [result# ~form]
     (println "DEBUG:" '~form "=>" result#)
     result#))

This really is the most simple version there is. If you want to know what a particular expression you cannot for some reason simply extract returns, add ?? as the first element of that expression.

(+ 2 3 (* 4 5) (?? - 5 3))
DEBUG: (- 5 3) => 2
=> 27

2. The regular, abridged debug:

(defmacro ? [& form]
  `(let [result# ~form]
     (println "DEBUG:" '~(map #(if (coll? %)
                                 "..."
                                 (str %)) form) "=>" result#)
     result#))

This one will replace sub-forms in the form with "...". This is useful if you don't want your output too cluttered:

(? + 2 3 (* 4 5) (- 5 3))
DEBUG: (+ 2 3 ... ...) => 27
=> 27

3. The deep inspect:

(defmacro ??? [& form]
  `(? ~@(map #(if (coll? %)
                `(??? ~@%)
                %) form)))

This one places an abridged debug "?" in every form in the tree in which it is placed. For example:

(??? + 2 3 (* 4 5) (- 5 3))
  is equivalent to
(? + 2 3 (? * 4 5) (? - 5 3))

Thus giving:

(??? + 2 3 (* 4 5) (- 5 3))
DEBUG: (* 4 5) => 20
DEBUG: (- 5 3) => 2
DEBUG: (+ 2 3 ... ...) => 27
=> 27

So, recapping, add one to three question marks, the more you put, the more info you get.

It would be, of course, trivial to implement ????, which would be equivalent to a tree full of ??'s, but what's the point? Since ??? is intended to be used on trees, not shallow expressions, the verbosity of ?? on every level would be thorough overkill.

Multi-set!:

 

(defmacro multi-set! [target & pairs]
  (let [x (gensym)]
    `(let [~x ~target]
       ~@(for [[field value] pairs]
           `(set! (. ~x ~field) ~value)))
       ~x)))

Due to working with java.awt.GridBagConstraints a lot recently, I got annoyed with how often I have to rewrite the line (.set! (.[some field] c) [some value]). So, being lazy as hell, I'd much rather write

(multi-set! [instance-expr]
  ([field] [value])
  ([field] [value])
  ([field] [value])
  ...)

In fact, I wrote a macro called "constraints" for working with GridBagConstraints, but it's a tad too long to share here. Also, it's boring.

This macro actually uses a fun trick, aka displays a subtle problem with the auto-gensym feature (symbols with # appended are replaced with a unique name, in order to avoid name clashes).

If you were to write...

(defmacro multi-set-wrong! [target & pairs]
  (let [x# ~target]
    ~@(for [[field value] pairs]
        `(set! (. x# ~field) ~value))
    x#))

In this case, the auto-gensymmed symbols x# in the let-form and inside the set!-form are DIFFERENT symbols, because they are within DIFFERENT quotes. So, this version doesn't work as intended. In fact, the generated (set! ...) expressions will throw an exception, because the symbol x# in them could not be found.

Thus, in the actually working version, we create a symbol by hand, and use that symbol throughout the macro. This is a trick I learned from reading the source of the doto macro (I had a hunch I'd find something useful there, because the macro works similarly to mine - I was right!).

Convenient Map Writing Macros: 


More than once, I've written macros that simply implement a more convenient syntax for maps. For example, since I'm learning japanese right now, I wrote a program to quiz me on Hiragana. For that, I had to store all corresponding Unicode characters in the program, and map them to the Romaji (latin) transcriptions.

(defmacro defseries [group & chars]
  `(def ~group
     ~(into [] (map (fn [[name code]]
                      (let [code (+ code 0x3000)]
                         {:name name, :code code}))
                     chars))))

While this wasn't the perfect approach in this case (here, I still had to type char => pairs, just with smaller values and less overhead), but it still vastly reduced the typing I had to do. I do things like that quite frequently.

(defseries a ("a" 0x42) ("i" 0x44) ("u" 0x46) ("e" 0x48) ("o" 0x4A))

I typed out roughly 15-16 series like that. So I did save me some typing with just a tad of thinking.

That's it for now.

N.