learnxinyminutes-docs/clojure-macros.md

150 lines
4.3 KiB
Markdown
Raw Normal View History

2014-01-20 04:33:06 +00:00
---
2024-10-20 21:46:35 +00:00
language: Clojure macros
2014-01-20 04:33:06 +00:00
filename: learnclojuremacros.clj
contributors:
- ["Adam Bard", "http://adambard.com/"]
---
As with all Lisps, Clojure's inherent [homoiconicity](https://en.wikipedia.org/wiki/Homoiconic)
gives you access to the full extent of the language to write code-generation routines
called "macros". Macros provide a powerful way to tailor the language to your needs.
Be careful though. It's considered bad form to write a macro when a function will do.
Use a macro only when you need control over when or if the arguments to a form will
be evaluated.
You'll want to be familiar with Clojure. Make sure you understand everything in
2024-12-09 11:21:58 +00:00
[Clojure in Y Minutes](../clojure/).
2014-01-20 04:33:06 +00:00
```clojure
;; Define a macro using defmacro. Your macro should output a list that can
;; be evaluated as clojure code.
;;
;; This macro is the same as if you wrote (reverse "Hello World")
(defmacro my-first-macro []
(list reverse "Hello World"))
;; Inspect the result of a macro using macroexpand or macroexpand-1.
;;
;; Note that the call must be quoted.
(macroexpand '(my-first-macro))
;; -> (#<core$reverse clojure.core$reverse@xxxxxxxx> "Hello World")
;; You can eval the result of macroexpand directly:
(eval (macroexpand '(my-first-macro)))
; -> (\d \l \o \r \W \space \o \l \l \e \H)
;; But you should use this more succinct, function-like syntax:
(my-first-macro) ; -> (\d \l \o \r \W \space \o \l \l \e \H)
;; You can make things easier on yourself by using the more succinct quote syntax
;; to create lists in your macros:
(defmacro my-first-quoted-macro []
'(reverse "Hello World"))
(macroexpand '(my-first-quoted-macro))
;; -> (reverse "Hello World")
;; Notice that reverse is no longer function object, but a symbol.
;; Macros can take arguments.
(defmacro inc2 [arg]
(list + 2 arg))
(inc2 2) ; -> 4
;; But, if you try to do this with a quoted list, you'll get an error, because
;; the argument will be quoted too. To get around this, clojure provides a
;; way of quoting macros: `. Inside `, you can use ~ to get at the outer scope
(defmacro inc2-quoted [arg]
`(+ 2 ~arg))
(inc2-quoted 2)
;; You can use the usual destructuring args. Expand list variables using ~@
(defmacro unless [arg & body]
`(if (not ~arg)
(do ~@body))) ; Remember the do!
(macroexpand '(unless true (reverse "Hello World")))
;; ->
;; (if (clojure.core/not true) (do (reverse "Hello World")))
;; (unless) evaluates and returns its body if the first argument is false.
;; Otherwise, it returns nil
(unless true "Hello") ; -> nil
(unless false "Hello") ; -> "Hello"
;; Used without care, macros can do great evil by clobbering your vars
(defmacro define-x []
'(do
(def x 2)
(list x)))
(def x 4)
(define-x) ; -> (2)
(list x) ; -> (2)
;; To avoid this, use gensym to get a unique identifier
(gensym 'x) ; -> x1281 (or some such thing)
(defmacro define-x-safely []
(let [sym (gensym 'x)]
`(do
(def ~sym 2)
(list ~sym))))
(def x 4)
(define-x-safely) ; -> (2)
(list x) ; -> (4)
;; You can use # within ` to produce a gensym for each symbol automatically
(defmacro define-x-hygienically []
2014-01-20 04:33:06 +00:00
`(do
(def x# 2)
(list x#)))
(def x 4)
(define-x-hygienically) ; -> (2)
2014-01-20 04:33:06 +00:00
(list x) ; -> (4)
;; It's typical to use helper functions with macros. Let's create a few to
;; help us support a (dumb) inline arithmetic syntax
2014-01-20 04:33:06 +00:00
(declare inline-2-helper)
(defn clean-arg [arg]
(if (seq? arg)
(inline-2-helper arg)
arg))
(defn apply-arg
"Given args [x (+ y)], return (+ x y)"
[val [op arg]]
(list op val (clean-arg arg)))
(defn inline-2-helper
[[arg1 & ops-and-args]]
(let [ops (partition 2 ops-and-args)]
(reduce apply-arg (clean-arg arg1) ops)))
;; We can test it immediately, without creating a macro
(inline-2-helper '(a + (b - 2) - (c * 5))) ; -> (- (+ a (- b 2)) (* c 5))
; However, we'll need to make it a macro if we want it to be run at compile time
(defmacro inline-2 [form]
(inline-2-helper form))
2014-01-20 04:33:06 +00:00
(macroexpand '(inline-2 (1 + (3 / 2) - (1 / 2) + 1)))
; -> (+ (- (+ 1 (/ 3 2)) (/ 1 2)) 1)
(inline-2 (1 + (3 / 2) - (1 / 2) + 1))
; -> 3 (actually, 3N, since the number got cast to a rational fraction with /)
```
### Further Reading
[Writing Macros](http://www.braveclojure.com/writing-macros/)
2014-01-20 04:33:06 +00:00
[Official docs](http://clojure.org/macros)
2014-01-20 04:33:06 +00:00
[When to use macros?](https://lispcast.com/when-to-use-a-macro/)