{:draft ["true"]}

Index

Macros

Macro programming

  • Macros are functions that take code as input and generates code as output.
  • Recall that code is data in Clojure. All Clojure code is expressed as a mixture of nested lists and vectors.
In [1]:
;
; This is a macro that generates the code to print "Hello"
;
(defmacro hello []
    (list 'print "Hello"))
Out[1]:
#'user/hello
In [2]:
(hello)
Hello
Out[2]:
nil
In [3]:
;
; Clojure provides a facility (macroexpand) that allows us
; to see what code is generated from a macro call.
;
(macroexpand '(hello))
Out[3]:
(print "Hello")
In [5]:
;
; This shows how limiting a function is.  It cannot
; examine _code_ as data.
;
(defn hello [expr]
    (println "Hello: " expr))
Out[5]:
#'user/hello
In [6]:
;
; Note, the input to `hello` is the result of the code, not the code itself.
;
(hello (+ 1 2 3))
Hello:  6
Out[6]:
nil
In [7]:
;
; But macro gets the code as data *before* it is evaluated.
;
(defmacro hello [expr]
    (let [expr-as-str (str expr)]
        (list println "Hello" expr-as-str)))
Out[7]:
#'user/hello
In [8]:
(macroexpand '(hello (+ 1 2 3)))
Out[8]:
(#function[clojure.core/println] "Hello" "(+ 1 2 3)")
In [9]:
(hello (+ 1 2 3))
Hello (+ 1 2 3)
Out[9]:
nil

Clojure's programming constructs that help us to generate code

Suppose we want to generate

(let [x 10] (inc x))
In [15]:
;
; From first principle, very very impractical
;
(list 'let (vector 'x 10) (list clojure.core/inc 'x))
Out[15]:
(let [x 10] (#function[clojure.core/inc] x))
In [19]:
;
; Clojure offers the *backtick* escape form.
; *NOTE*:
; backtick: `
; singlequote: '
; We want to use the backtick form.
;

`(let [x 10] (inc x))
Out[19]:
(clojure.core/let [user/x 10] (clojure.core/inc user/x))
In [20]:
;
; The backtick allows us to disable the escape mode.
;
(defn get-limit[] (rand-int 100))
Out[20]:
#'user/get-limit
In [23]:
; Want to generate this
; (let [x __] (inc x))
;
`(let [x ~(get-limit)]
     (inc x))
Out[23]:
(clojure.core/let [user/x 18] (clojure.core/inc user/x))

Note: The tilda ~(...) form is executed during macro expansion phase, and the result is inserted into the quoted form.

There is another macro expansion phase form, called slicing. Suppose we want to generate the following code:

(println ____ _____ _____)

In [25]:
(defn get-limits []
    (take 3 (repeatedly get-limit)))
Out[25]:
#'user/get-limits
In [27]:
(get-limits)
Out[27]:
(38 5 27)
In [30]:
;
; Here the insertion by ~ is not sufficient.  We don't want to insert back a list,
; but we want to insert the result as a sublist of the (println   .... ) form.
;
`(println ~(get-limits))
Out[30]:
(clojure.core/println (40 96 57))

The ~@expr will execute expr in macro expansion phase, and insert the result as a list back into the form one element at a time.

In [31]:
`(println ~@(get-limits))
Out[31]:
(clojure.core/println 18 26 33)

Note:

There is one more code generate feature, symbol generation.

Let's write some interesting macros

In [33]:
(defmacro verbose [expr]
    ; Remember expr is a data fragment
    `(do (println ~(str expr))
         (println "=>" ~expr)))
Out[33]:
#'user/verbose
In [34]:
(verbose (+ 1 2 3))
(+ 1 2 3)
=> 6
Out[34]:
nil
In [37]:
(verbose (repeat 3 (+ 1 2)))
(repeat 3 (+ 1 2))
=> (3 3 3)
Out[37]:
nil
In [40]:
; Don't forget the single quote the form you
; want to expand.
(clojure.pprint/pprint
    (macroexpand '(verbose (repeat 3 (+ 1 2)))))
(do
 (clojure.core/println "(repeat 3 (+ 1 2))")
 (clojure.core/println "=>" (repeat 3 (+ 1 2))))
Out[40]:
nil

Infix operation form

In [41]:
(+ 1 2)
Out[41]:
3
In [42]:
(+ 1 2 3 4 5 67 8)
Out[42]:
90
In [44]:
(defmacro infix [form]
    (let [[a op b] form]
        `(~op ~a ~b)))
Out[44]:
#'user/infix
In [45]:
(infix (1 + 2))
Out[45]:
3
In [46]:
(macroexpand '(infix (10 * 20)))
Out[46]:
(* 10 20)

Build a timer macro

(--- (apply + (range 1e6)))

; => (apply = (range 1e6)) took 100 ms
847298729357
In [51]:
(defn now [] (System/currentTimeMillis))
(now)
Out[51]:
1606404947079
In [74]:
(defmacro ---
    [expr]
    `(let [start# (now)
           result# ~expr
           duration# (- (now) start#)]
         (println ~(str expr) "took" duration# "ms")
         result#))
Out[74]:
#'user/---
In [76]:
(clojure.pprint/pprint
    (macroexpand-1 '(--- (+ 1 2))))
(clojure.core/let
 [start__5576__auto__
  (user/now)
  result__5577__auto__
  (+ 1 2)
  duration__5578__auto__
  (clojure.core/- (user/now) start__5576__auto__)]
 (clojure.core/println "(+ 1 2)" "took" duration__5578__auto__ "ms")
 result__5577__auto__)
Out[76]:
nil
In [78]:
(--- (apply + (range 1e6)))
(apply + (range 1000000.0)) took 54 ms
Out[78]:
499999500000
In [ ]: