Skip to content

Commit 57eb031

Browse files
authored
Merge pull request #1 from Homo-carbonis/patch-2
Create macros.md
2 parents f5d6b69 + a44cb34 commit 57eb031

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed

docs/macros.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Macros
2+
One of the most distinctive features of Common Lisp is its macros. In most other languages, code is strutured according to a fairly complex syntax, which is parsed to generate an abstract syntax tree to be processed further by the compiler or interpreter and is not generally available to the programmer. Lisp code is already structed as a tree of s-expressions, read in as a simple cons structure which can be manipulated in the same way as any other data. This equivalance between code and data is described by the Greek word "homoiconicity" and it makes Lisp macros particularly powerful and easy to write.
3+
4+
## Examples
5+
Macros have multiple uses. They can introduce new syntax to the language, control when code is evaluated and how many times, elminate repetitive boilerplate or make programs more efficient by doing computation at compile time.
6+
7+
## AND
8+
Suppose we have some lisp forms and we want to check they all evaluate to true. The obvious way to write this is (and a b c), where a b and c are some arbitrary lisp forms. We could implement this as a function, using rest parameters and recursion to operate on an arbitrary number of values before returning either the final value, or nil if any of the other values are nil.
9+
```lisp
10+
(defun and (&rest values)
11+
(when (car values)
12+
(if (cdr values)
13+
(and (cdr values))
14+
(car values))))
15+
```
16+
```lisp
17+
* (and (< 1 2) 3)
18+
3
19+
* (and (> 1 2) 3)
20+
nil
21+
```
22+
This seems to work, but suppose we are writing a control system for a rocket silo. We want to perform a sequence of operations, checking each stage returns a true value to indicate it has been successfully completed.
23+
```lisp
24+
* (and (open-doors) (prime-fuel-tanks) (launch-rocket))
25+
```
26+
Why did our silo just blow up?
27+
28+
These are all functions with side effects. Calling open-doors sends a signal to open the doors and then returns t if sensors indicate the doors have opened correctly or nil otherwise. When a function is called all of its arguments are all evaluated first, followed by the body of the function. This means that our `and` expression will try to open the doors, prime the fuel tanks and launch the rocket, and only afterwards check the doors opened successfully.
29+
30+
With a macro we can control exactly when the evaluation happens
31+
32+
```lisp
33+
(defmacro %and (&rest forms)
34+
(list 'when (car forms)
35+
(if (cddr forms)
36+
(cons '%and (cdr forms))
37+
(cadr forms))))
38+
```
39+
40+
Our missile silo controller is expanded at compile time to
41+
42+
```lisp
43+
(if (open-doors)
44+
(if (prime-fuel-tanks)
45+
(launch-rocket)))
46+
```
47+
If an argument evaluates to nil, the chain of `if` statements is broken and none of the remaining arguments are evaulated.
48+
49+
## Backquote
50+
Common Lisp's backquote syntax is very useful tool for constructing code in macros and complex data structures generally. Instead of constructing a data structure with functions like `cons` `list` and `append`, one can simple write a quoted template and insert values into it by unquoting with commas.
51+
For instance, instead of
52+
```lisp
53+
(append '(there will be) (incf n) '(green bottles standing on the wall))
54+
```
55+
56+
With backquote we can write
57+
```lisp
58+
* (defvar n 10)
59+
N
60+
* `(there will be ,(decf n) green bottles standing on the wall)
61+
(there will be 9 green bottles standing on the wall)
62+
* `(there will be ,(decf n) green bottles standing on the wall)
63+
(there will be 8 green bottles standing on the wall)
64+
```
65+
The comma unquotes the expression following it so it is evaluated as if it were outside the quote. n is decremented and the result is inserted into the quoted structure.
66+
67+
Lists can also be unquoted with `,@` which splices the contents of the list into the surrounding structure. Compare
68+
```lisp
69+
* (defvar object '(green bottle))
70+
OBJECT
71+
* `(there will be 1 ,object standing on the wall)
72+
(there will be 1 (green bottle) standing on the wall)
73+
* `(there will be 1 ,@object standing on the wall)
74+
(there will be 1 green bottle standing on the wall)
75+
```
76+
77+
Backquote is extremely useful for generating code in macros. Subsequent examples will make heavy use of it.
78+
79+
## Comparing numbers
80+
Quite a program needs to compare numbers and do something different depending on which is larger. In Lisp we could use a `cond` form like this
81+
```lisp
82+
(cond ((< x y) (y-is-bigger))
83+
((= x y) (both-equal))
84+
(t (x-is-bigger)))
85+
```
86+
This is quite long winded for such a common pattern. It would be clearer and more convenient if we could write something like `(compare (x y) (y-is-bigger) (x-is-bigger) (both-equal))`. Luckily with macros we can.
87+
88+
```lisp
89+
(defmacro compare ((a b) < = >)
90+
`(cond ((< ,a ,b) ,<)
91+
((= ,a ,b) ,=)
92+
(t ,>)))
93+
```
94+
```lisp
95+
* (let ((x 100)
96+
(y 10))
97+
(compare (x y) 'y-is-bigger 'both-equal 'x-is-bigger))
98+
X-IS-BIGGER
99+
```
100+
This appears to work but there is a problem. If the first test, `(< ,a ,b)`, fails, a and b are evaluated again in the second test where we have `(= ,a ,b)`. Arguments to a macro are not evaluated until after the macro is expanded, so if we insert `a` twice, it will be evaluated twice. For functions which have side effects or require a lot of computation, this is a problem.
101+
102+
We can solve this problem by generating code to evaluating a and b and bind them to variables before comparing them.
103+
104+
```lisp
105+
(defmacro compare ((a b) < = >)
106+
`(let ((a ,a)
107+
(b ,b))
108+
(cond ((< a b) ,<)
109+
((= a b) ,=)
110+
(t ,>))))
111+
```
112+
```lisp
113+
* (let ((x 11)
114+
(y 10))
115+
(compare ((decf x) y) 'b-is-bigger 'both-equal 'a-is-bigger))
116+
BOTH-EQUAL
117+
```
118+
Good, now `(decf x)` is only done once and the result is equal to y. Have we finished? How about this?
119+
120+
```lisp
121+
* (let ((x 10)
122+
(y 100)
123+
(a 'y-is-bigger)
124+
(b 'both-equal)
125+
(c 'x-is-bigger))
126+
(compare (x y) a b c))
127+
10
128+
```
129+
What has happened here? We expected to get back the value of a: `'y-is-bigger`. Instead of which the value of x has leaked through, replacing the value we supplied. Let's look at the expansion of this macro (we can get the result of expanding a macro with `macroexpand`)
130+
131+
```lisp
132+
* (macroexpand '(compare (x y) a b c))
133+
(LET ((A X) (B Y))
134+
(COND ((< A B) A) ((= A B) B) (T C)))
135+
```
136+
Looking at the expanded macro it's quite obvious what is going wrong. `x` is bound to `a` in the code produced by the macro which overrides the outer binding of a to 'y-is-bigger. We could get around this by coming up with more obscure names for bindings in macros, but there is a better solution. The function `gensym` returns a new uninterned symbol. Gensyms are guarenteed to be unique because no uninterned symbol is eq to any other, so if we bind a value to a gensym, we can be certain no other variables will be accidentally captured.
137+
138+
```lisp
139+
(defmacro compare ((a b) < = >)
140+
(let ((asym (gensym))
141+
(bsym (gensym)))
142+
`(let ((,asym ,a)
143+
(,bsym ,b))
144+
(cond ((< ,asym ,bsym) ,<)
145+
((= ,asym ,bsym) ,=)
146+
(t ,>)))))
147+
```
148+
This can be expressed far more elegantly using the `once-only` macro, originally written by Peter Norvig and available in the Alexandria utility library. `once-only` automatically introduces gensym bindings for expressions in exactly the same way as the previous example.
149+
```lisp
150+
(require 'alexandria)
151+
(import alexandria:once-only)
152+
153+
(defmacro compare ((a b) < = >)
154+
(once-only (a b)
155+
`(cond ((< ,a ,b) ,<)
156+
((= ,a ,b) ,=)
157+
(t ,>))))
158+
```

0 commit comments

Comments
 (0)