Clojure: My first attempt at a macro
I’m up to the chapter on using macros in Stuart Halloway’s ‘Programming Clojure’ book and since I’ve never used a language which has macros in before I thought it’d be cool to write one.
In reality there’s no reason to create a macro to do what I want to do but I wanted to keep the example simple so I could try and understand exactly how macros work.
I want to create a macro which takes in one argument and then prints hello and the person’s name.
In the book Halloway suggests that we should start with the expression that we want to end up with, so this is what I want:
(println "Hello" person)
My first attempt to do that was:
(defmacro say-hello [person] println "Hello" person)
I made the mistake of forgetting to include the brackets around the ‘println’ expression so it doesn’t actually pass ‘“Hello”’ and ‘person’ to ‘println’. Instead each symbol is evaluated individually.
When we evaluate this in the REPL we therefore don’t quite get what we want:
user=> (say-hello "mark") "mark"
Expanding the macro results in:
user=> (macroexpand-1 '(say-hello "Mark")) "Mark"
Which is the equivalent of doing this:
user=> (eval (do println "hello" "Mark")) "Mark"
As I wrote previously this is because ‘do’ evaluates each argument in order and then returns the last one which in this case is “Mark”.
I fixed that mistake and got the following:
(defmacro say-hello [person] (println "Hello" person))
Which returns the right result…
user=> (say-hello "Mark") Hello Mark nil
…but actually evaluated the expression rather than expanding it because I didn’t escape it correctly:
user=> (macroexpand-1 '(say-hello "Mark")) Hello Mark nil
After these failures I decided to try and change one of the examples from the book instead of my trial and error approach.
One approach used is to build a list of Clojure symbols inside the macro definition:
(defmacro say-hello [person] (list println "hello" person))
user=> (macroexpand-1 '(say-hello "Mark")) (#<core$println__5440 clojure.core$println__5440@681ff4> "hello" "Mark")
This is pretty much what we want and although the ‘println’ symbol has been evaluated at macro expansion time it doesn’t actually make any difference to the way the macro works.
We can fix that by escaping ‘println’ so that it won’t be evaluated until evaluation time:
(defmacro say-hello [person] (list 'println "hello" person))
user=> (macroexpand-1 '(say-hello "Mark")) (println "hello" "Mark")
I thought it should also be possible to quote(‘) the whole expression instead of building up the list:
(defmacro say-hello [person] '(println "hello" person))
This expands correctly but when we try to use it this happens:
user=> (say-hello "Mark") java.lang.Exception: Unable to resolve symbol: person in this context
The problem is that when we use quote there is no evaluation of any of the symbols in the expression so the symbol ‘person’ is only evaluated at runtime and since it hasn’t been bound to any value we end up with the above error.
If we want to use the approach of non evaluation then we need to make use of the backquote(`) which stops evaluation of anything unless it’s preceded by a ~.
(defmacro a [person] `(println "hello" ~person))
This allows us to evaluate ‘person’ at expand time and replace it with the appropriate value.
In hindsight the approach I took to write this macro was pretty ineffective although it’s been quite interesting to see all the different ways that I’ve found to mess up the writing of one!
Thanks to A. J. Lopez, Patrick Logan and fogus for helping me to understand all this a bit better than I did to start with!