Macro Design - by example

24 views
Skip to first unread message

Meikel Brandmeyer

unread,
Jun 8, 2009, 5:50:47 PM6/8/09
to clo...@googlegroups.com
Dear Clojurians,

in the past there were several discussions on the list
regarding the one or the other macro. Please let me
show some design principles, which I believe are good
ideas for macro design.

On the occasion of a question in another thread we will
write a small helper macro to add an action listener to
a Swing component.

Let's start with the code we want to produce.

(.addActionListener component
(proxy [ActionListener] []
(actionPerformed [evt] (do-something-with evt) (do-more-stuff))))

This should be easy to translate into a macro.

(defmacro add-action-listener
[component & body]
`(.addActionListener ~component
(proxy [ActionListener] []
(~'actionPerformed [~'evt] ~@body))))

Note the ugly actionPerformed quoting. The invocation
looks like:

(add-action-listener component
(do-something-with evt)
(do-more-stuff))

Note that we had to capture "evt" to get access to
the event, which is passed to the handling code. This
means we get into trouble, when we want to access an
"external" evt in our handling code. This is ugly.

So we specify the name to use explicitly.

(defmacro add-action-listener
[component evt & body]
`(.addActionListener ~component
(proxy [ActionListener] []
(~'actionPerformed [~evt] ~@body))))

Note the slightly different quoting of evt. Again the
invocation:

(add-action-listener component the-evt
(do-something-with the-evt)
(do-more-stuff))

Hurray. We have a working macro. Time for refactoring.
We should ask ourselves: "Why do we actually need a macro?"
Macro writing is hard and one should keep the macros as
small as possible if not avoiding them at all.

Of course one might argue: "Yadda yadda. That's just a
style question." So here is another motivation.

Suppose we have some-handler-fn which we want to us in
our action listener. Using our macro this would look
like this:

(add-action-listener component the-evt
(some-handler-fn the-evt))

Bleh. Now this is ugly and verbose. We have to think
about a name for a thing which we just immediately pass
on to a function. Why should we bother with such details?
So let's write a function for that.

(defn add-action-listener*
[component handler]
(.addActionListener component
(proxy [ActionListener] []
(actionPerformed [evt] (handler evt)))))

Now the invocation looks like this:

(add-action-listener* component some-handler-fn)

Much more concise and to the spot. And note: in the
function we didn't have to mess around with strange
quotations. We just chose whatever name we liked for
the event argument without danger of interference.

But wait. Now we implemented the same functionality
twice. We should DRY this moisture.

As we saw, we can easily achieve the desired effect
if we have a handler function. So why not base the
macro on the function?

(defmacro add-action-listener
[component evt & body]
`(add-action-listener* ~component (fn [~evt] ~@body)))

Note the trick: we created a function which takes
the given event name as argument and has body as
it's body. So the event parameter is available in
the body under the given name. The anonymous fn
is then simply passed to the star function.

"Ah. More yadda yadda. I don't have handler fns!"

So why would it still be a good idea to have the
macro use some driver function?

Suppose you are hunting a bug. Your suspicion is
that there is some trouble with the event handling.
So you want to log some message about the event.
You start changing the macro.

(defmacro add-action-listener
[component evt & body]
`(.addActionListener ~component
(proxy [ActionListener] []
(~'actionPerformed [~evt] (println "Got event:" ~evt) ~@body))))

And now you can track the event passing. Eh.. well..
Not quite! First you have to recompile all your code
and restart the application. Since a macro gets
expanded at compile time, there is no way to change
the expansion afterwards.

Consider the same change to the function version.
Now all you have to do is to just reload the single
function in the running application. This can be
easily done by environments like SLIME or VimClojure.
Et voila. The next time the function is called you already
get the benefit. Without recompilation of the whole app...

So here the complete macro-function pair + invocation:

(defn add-action-listener*
[component handler]
(.addActionListener component
(proxy [ActionListener] []
(actionPerformed [evt] (handler evt)))))

(defmacro add-action-listener
[component evt & body]
`(add-action-listener* ~component (fn [~evt] ~@body)))

(add-action-listener component the-evt
(do-something-with the-evt)
(do-more-stuff))

(add-action-listener* component some-handler-fn)

So to summarise:

- write the code which you want in your macro
- write a macro which expands into the code with
variable parts as macro arguments.
- extract the parts which are not required to be
a macro as a function
- modify the macro to simply call the function

Surprisingly many macros can be handled like this.

I hope this helps you to write more robust and
flexible macros.

Sincerely
Meikel


Laurent PETIT

unread,
Jun 9, 2009, 3:29:14 AM6/9/09
to clo...@googlegroups.com
Good !

Do you consider putting this on the clojure wikibook ?

Cheers,

--
Laurent

2009/6/8 Meikel Brandmeyer <m...@kotka.de>:

Daniel Lyons

unread,
Jun 9, 2009, 4:42:06 AM6/9/09
to clo...@googlegroups.com

On Jun 8, 2009, at 3:50 PM, Meikel Brandmeyer wrote:

> And now you can track the event passing. Eh.. well..
> Not quite! First you have to recompile all your code
> and restart the application. Since a macro gets
> expanded at compile time, there is no way to change
> the expansion afterwards.

Another problem is that you cannot use macros or special forms with
higher-order functions. If your macro looks a lot like a function, you
or someone you love may be tempted to use it from a higher-order
function. This will end in tears:

user> (defmacro square-m [x] `(* ~x ~x))
#'user/square-m
user> (square-m 5)
25
user> (macroexpand '(square-m 5))
(clojure.core/* 5 5)

user> (defn square-f [x] (* x x))
#'user/square-f
user> (apply square-f '(3))
9

user> (apply square-m '(3))
Can't take value of a macro: #'user/square-m
[Thrown class java.lang.Exception]

user> (map square-f (range 5))
(0 1 4 9 16)

user> (map square-m (range 5))
Can't take value of a macro: #'user/square-m
[Thrown class java.lang.Exception]

IMO, macros are a wonderful device for introducing syntactic sugar.
Seibel's define-binary-class in PCL is a good example of a substantial
improvement:

http://gigamonkeys.com/book/practical-parsing-binary-files.html

Unless it's a visual, syntactic benefit which will eliminate
substantial boilerplate, I wouldn't go near a macro. Across all my
Common Lisp code I have 1 macro for about every 32 functions. I
suspect this value is not representative of actual practice because my
Lisp was exploratory and short. Clojure's own source code has 65
macros to 464 functions, or about 1 to 8 (according to "grep -ir defn
* | wc -l" vs. "grep -ir defmacro * | wc -l"). Just another something
to keep in mind when considering using a macro: they should be
uncommon or rare compared to functions.

> So to summarise:
>
> - write the code which you want in your macro
> - write a macro which expands into the code with
> variable parts as macro arguments.
> - extract the parts which are not required to be
> a macro as a function
> - modify the macro to simply call the function

I think this is extremely good advice.

Thanks!


Daniel Lyons
http://www.storytotell.org -- Tell It!

Emeka

unread,
Jun 10, 2009, 4:39:39 AM6/10/09
to clo...@googlegroups.com
Meikel,

Could I be allowed to join your online class:)

Regards,
Emeka


Reply all
Reply to author
Forward
0 new messages