Clojure
as any other LISP
language is homoiconic: the code written in the language is encoded as data structures that the language has tools to manipulate.
The most powerful tool to manipulate code is the macro.
We all know how to call macros in our clojure
code. But have you ever asked yourself:
What is exactly the difference between a macro and a function?
or:
What would it take to replace macro calls with function calls in a
clojure
program?
In this article, we will try to give an answer by stating two assertions and demonstrating them with live code in KLIPSE.
Macros vs. functions
One common explanation of the difference between functions and macros is:
- a function transforms values into other values.
- a macro transforms code into other code.
We want to give a more precise answer, analysing two aspects of functions and macros:
- when the arguments are evaluated?
- is the return value evaluated?
Regarding to those two aspects, here are the differences between macros and functions:
arguments evaluation | return value evaluated? | |
---|---|---|
functions | before function code execution | not evaluated |
macros | only when macro code evaluates them explicitly | evaluated |
The proof for functions
About functions, we stated that:
- function arguments (the input) are evaluated before the function code execution
- function return value (the output) is not evaluated
Let’s check it by writing a function with different side effects in the input and the output. We’ll see what side effect is executed.
We will modify the color and the border of the klispe container as side effects, with a few helpers:
(ns my.m$macros)
(defn set-color! [elem c]
(set! (.. elem -style -color) c))
(defn set-border [elem border]
(set! (.. elem -style -border) border))
(defn reset-elem! [elem]
(set! (.-innerHTML elem) "This is a sentence")
(set! (.-style elem) nil))
(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.)
Now, let’s check what happens when we call a function:
(defn color-me-fn [arg]
(list 'set-border 'js/klipse-container "solid 3px red"))
(reset-elem! js/klipse-container)
(color-me-fn (set-color! js/klipse-container "red"))
As you can see above, the color of the text is red but the border is un-mondified: it means that the input is evaluated and the output is not evaluated; the output is returned, but it’s not evaluated.
Q.E.D.∎
The proof for macros
About macros, we stated that:
- macros arguments (the input) are not evaluated before the macro code evaluates them explicitly
- macros return value (the output) is evaluated
Let’s check it by writing a macro with different side effects in the input and the output. The macro will not evaluate the input and we’ll see what side effect is executed.
(defmacro color-me-macro [arg]
(list 'set-border 'js/klipse-container "solid 3px red"))
(reset-elem! js/klipse-container)
(my.m/color-me-macro (set-color! js/klipse-container "red"))
As you can see above, the color of the text is un-modified but the border is red: it means that the input is not evaluated and the output is evaluated.
Q.E.D.∎
Two assertions about functions and macros
Now we will try to see how functions and macros are interchangeable.
Let’s say we have a function foo-f
and a macro foo-m
with exactly the same code. Then, the following two assertions hold:
Assertion #1: the macro expansion is equivalent to the function execution with arguments quoted
(= (macroexpand-1 '(foo-m arg1 arg2 ...))
(foo-f 'arg1 'arg2 '...))
Assertion #2: the macro execution is equivalent to the evaluation of the function execution with arguments quoted
(= (foo-m arg1 arg2 ...)
(eval (foo-f 'arg1 'arg2 '...)))
First assertion - macro expansion
The first assertion states that the macro expansion is equivalent to the function execution with arguments quoted.
(= (macroexpand-1 '(foo-m arg1 arg2 ...))
(foo-f 'arg1 'arg2 '...))
Let’s check it with KLIPSE, using the simple when
macro as defined in core.clj:
Second assertion - function evaluation
The second assertion states that the macro execution is equivalent to the evaluation of the function execution with arguments quoted.
(= (foo-m arg1 arg2 ...)
(eval (foo-f 'arg1 'arg2 '...)))
In order to check the second assertion, we have to define eval
in clojurescript
: this is easily done with self-host function eval-str
:
(You can read more about self-host clojurescript
in Self-Hosted Clojurescript in action - Part 1.)
After having illustrated the differences between macros and functions, we will show step-by-step how to write the disp
macro that we use so much in our blog (including in this article). Hopefully, it will illustrate and clarify all the basic building blocks of macros in clojure
, namely:
- regular quote
- syntax quote
- fully-qualified symbols
- unquote
- unique symbol generation
- quote splicing
Follow us on twitter to get notified, when the next article in the macro tutorial series is published.
Clojure rocks!