After having understood the fundamental difference between functions and macros, we are now ready to write our first macro.
In this article, we want to write macros without the help of idiomatic tools that
clojure provides for writing macros (syntax quote, unquote,
gensym etc…). The purpose of this article it to let you understand why those powerful tools are mandatory, by experiencing how it feels to write a macro without those tools.
We will train ourselves with a simple macro that we use a lot for our blog posts: the
Kind of eating our own dog food…
Definition of the
disp macro receives expressions and returns a string with the expressions and their respective evaluations.
(disp (map inc [1 2 3]) (str "hello" " " "world") (true? 1)) ; (map inc [1 2 3]) => (2 3 4) ; (str "hello" " " "world") => "hello world" ; (true? 1) => false
In this article, for the sake of simplicity, we will limit ourselves to the case of a single expression.
Let’s start coding!
There are three parts in the
- expression quoting (keeping is at it is)
- expression evaluation (evaluating it)
- concatenation of the quoting and the evaluation
This namespace is going to be our playground for dealing with macros:
(If you wonder why we have to append
$macros to the namespace and to reference the fully-qualified macro with self-hosted
clojurescript, read Messing with Macros at the REPL.)
How to quote inside a macro
First, we need to understand how to quote an expression inside a macro. Let’s try to write a macro that receives an expression and return it, quoted.
(defmacro my-quote [form] (quote form))
It doesn’t work.
(my.repl/my-quote (map inc [1 2 3]))
The reason is that - as you can see with
(quote form) is
(macroexpand-1 '(my.repl/my-quote (map inc [1 2 3])))
Let’s try to state explicitly what is the requirement for the
my-quote macro: We need to return an expression that when it is evaluated, it becomes
(map inc [1 2 3]). The expression that fits this definition is
(quote (map inc [1 2 3])). In other words, it is a list whose first element is the symbol
quote and its second element is
(map inc [1 2 3]).
Now, try to update the klipse above with your correct implementation for the
(Even on your mobile device, it works: just wait 3 seconds and your code is automatically evaluated.)
If you cannot make it after a couple of trials, you can read my solution below…
Give it another try, before surrending…
Here is my solution:
(defmacro my-quote-fixed [form] (list 'quote form))
Now, it works:
(my.repl/my-quote-fixed (map inc [1 2 3]))
How to concatenate evaluations inside a macro
Now, we have to figure out how to concatenate evaluations inside a macro. I hope that the following example will clarify what I mean by that.
(concat-evaluations "hello" (+ 1 2)) ;"hello 3"
Let’s give it a try:
(defmacro concat-evaluations [a b] (str a " " b))
It seems to work:
(my.repl/concat-evaluations "hello" "world")
But in reality, it fails:
(my.repl/concat-evaluations "hello" (+ 1 2))
It fails because inside the macro, the value of
(+ 1 2), so the
str function appends
(+ 1 2) to “hello”.
my-quote, the idea is to return an expression that when evaluated it becomes
(str "hello" " " 3).
Try to find a solution of your own.
My solution is:
(defmacro concat-evaluations-fixed [a b] (list 'str a " " b))
And it works:
(my.repl/concat-evaluations-fixed "hello" (+ 1 2))
If you are curious, you can take a look at the expansion of the macro:
(macroexpand-1 '(my.repl/concat-evaluations-fixed "hello" (+ 1 2)))
Implementation of the
Now we are ready to implement the
disp macro, by assembling our building blocks:
(defmacro disp [form] (list 'str (list 'quote form) " => " form))
(my.repl/disp (map inc [1 2 3]))
(macroexpand-1 '(my.repl/disp (map inc [1 2 3])))
Really simple right?
There are a couple of issues with our naive implementation:
A. It’s really complicated to write macros this way: almost everything has to be embedded into a expression that begins with
B. The code generated by the macro doesn’t look like the code of the macro: in the generated code,
list doesn’t occur at all!
C. What happens if you define local variables inside a macro? They might conflict with the variables defined in the scope of the macro caller. Here is an illustration of this issue:
(defmacro concat-evaluations-sep [a b] (list 'let ['sep ": "] (list 'str a 'sep b)))
(my.repl/concat-evaluations-sep "hello" (map inc [100 2 3]))
(def sep 100)
(my.repl/concat-evaluations-sep "hello" (map inc [sep 2 3]))
(macroexpand-1 '(my.repl/concat-evaluations-sep "hello" sep))
D. Your macro will behave completely crazy when it is called inside a namespace where one of the functions that you use in the code of the macro is overriden.
Here is an example:
The 4 issues mentioned above are solved elegantly in
clojure with the idiomatic tools I mentioned in the introduction.
Here is a presentation of syntax quote.
After reading this article, you should be able to re-write the
disp macro, idiomatically.
Please share your best implementations in the comments below.