Skip to content

Commit ec993b0

Browse files
committed
add new blog post writing-your-first-macro
1 parent ee71ce5 commit ec993b0

File tree

1 file changed

+194
-0
lines changed

1 file changed

+194
-0
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
+++
2+
title = "Writing Your First Macro in Phel"
3+
aliases = [ "/blog/first-macro" ]
4+
description = "Learn how macros let you extend the language itself, with step-by-step examples that turn repetitive patterns into reusable syntax."
5+
+++
6+
7+
If you have played with [threading macros](/blog/threading-macros) or [pattern matching](/blog/pattern-matching), you have already been using macros without thinking about it. Now it is time to write your own.
8+
9+
PHP developers sometimes reach for `eval()` or code generation tools when they need to produce code dynamically. Clojure developers know a better way: macros. Phel brings that same power to the PHP ecosystem, letting you extend the language at compile time instead of juggling strings at runtime.
10+
11+
## Functions vs macros: when code is data
12+
13+
Regular functions receive values and return values. Macros receive *code* and return *code*. The transformation happens at compile time, so there is zero runtime cost.
14+
15+
Say you keep writing this pattern:
16+
17+
```phel
18+
(when (not logged-in?)
19+
(redirect "/login"))
20+
```
21+
22+
You could wrap it in a function, but then `logged-in?` would be evaluated before the function even sees it. With a macro you create actual new syntax:
23+
24+
```phel
25+
(defmacro unless [test & body]
26+
`(if ,test nil (do ,@body)))
27+
28+
(unless logged-in?
29+
(redirect "/login"))
30+
```
31+
32+
At compile time, Phel rewrites that call into an `if` form. Your condition stays lazy, just like the built-in control flow. Clojure folks will feel right at home; the syntax is nearly identical.
33+
34+
> Fun fact: Phel's core already has `when-not` which does exactly this. Peek at the source and you will find the same pattern we just wrote.
35+
36+
## Quote and unquote: treating code as data
37+
38+
Two concepts make macros tick: **quote** and **unquote**.
39+
40+
**Quote** (the `'` character) stops evaluation. It hands you raw code instead of running it:
41+
42+
```phel
43+
(+ 1 2) # => 3
44+
'(+ 1 2) # => (+ 1 2), just a list
45+
```
46+
47+
**Quasiquote** (the backtick `` ` ``) works like quote, but you can poke holes with **unquote** (`,`) to let specific pieces evaluate:
48+
49+
```phel
50+
`(1 2 ,(+ 1 2)) # => (1 2 3)
51+
```
52+
53+
Think of quasiquote as a template. Most of it stays literal; the `,` parts get filled in. If you have written Clojure, this is exactly what you know.
54+
55+
## Building `unless` step by step
56+
57+
```phel
58+
(defmacro unless
59+
"Evaluates body when test is false."
60+
[test & body]
61+
`(if ,test nil (do ,@body)))
62+
```
63+
64+
What is happening here:
65+
66+
- `defmacro` defines a macro, just like `defn` defines a function.
67+
- The docstring explains what the macro does.
68+
- `test` and `body` receive *unevaluated code*, not values.
69+
- The backtick starts a code template.
70+
- `,test` splices in the test expression; `,@body` splices the body expressions inline.
71+
72+
When you write:
73+
74+
```phel
75+
(unless (> x 10)
76+
(print "x is small")
77+
(log-warning "check the value"))
78+
```
79+
80+
Phel transforms it at compile time to:
81+
82+
```phel
83+
(if (> x 10) nil (do (print "x is small") (log-warning "check the value")))
84+
```
85+
86+
No runtime overhead, no string manipulation, no `eval()`.
87+
88+
## A practical example: timing code
89+
90+
Here is something you cannot do with a plain function. Say you want to measure how long a chunk of code takes. Phel's core has a `time` macro that does exactly this:
91+
92+
```phel
93+
(defmacro time
94+
"Evaluates expr and prints the time it took. Returns the value of expr."
95+
[expr]
96+
`(let [start$ (php/microtime true)
97+
ret$ ,expr]
98+
(println "Elapsed time:" (* 1000 (- (php/microtime true) start$)) "msecs")
99+
ret$))
100+
101+
(time (slow-operation))
102+
# Prints: Elapsed time: 142.3 msecs
103+
```
104+
105+
The `$` suffix is a convention for local bindings inside macros. It helps avoid name collisions with user code. The body runs between the two `microtime` calls, and you get the elapsed time printed for free.
106+
107+
In PHP you would wrap this in a closure or duplicate the timing code everywhere. The macro keeps it clean and reusable.
108+
109+
## Avoiding name collisions with gensym
110+
111+
The `$` suffix works for simple cases, but what if your macro could be nested or the user happens to use `start$` as a variable? For guaranteed uniqueness, use `gensym` to generate fresh symbols.
112+
113+
Here is how Phel's core implements `with-output-buffer`:
114+
115+
```phel
116+
(defmacro with-output-buffer
117+
"Everything printed inside the body is captured and returned as a string."
118+
[& body]
119+
(let [res (gensym)]
120+
`(do
121+
(php/ob_start)
122+
,@body
123+
(let [,res (php/ob_get_contents)]
124+
(php/ob_end_clean)
125+
,res))))
126+
127+
(with-output-buffer
128+
(print "Hello ")
129+
(print "World"))
130+
# => "Hello World"
131+
```
132+
133+
We call `gensym` outside the quasiquote to get a unique symbol, then unquote it with `,res` wherever we need it. No matter how many times you nest `with-output-buffer`, each expansion gets its own symbol.
134+
135+
## More patterns from Phel's core
136+
137+
**Short-circuit evaluation with `or`:**
138+
139+
```phel
140+
(defmacro or
141+
"Returns the first truthy value, or the last value."
142+
[& args]
143+
(case (count args)
144+
0 nil
145+
1 (first args)
146+
(let [v (gensym)]
147+
`(let [,v ,(first args)]
148+
(if ,v ,v (or ,@(next args)))))))
149+
```
150+
151+
Notice how `or` uses `gensym` because it recursively expands itself. Each level needs its own unique binding.
152+
153+
**Auto-logging function calls:**
154+
155+
```phel
156+
(defmacro defn-traced
157+
"Defines a function that logs when called."
158+
[name args & body]
159+
`(defn ,name ,args
160+
(println "Calling" ',name)
161+
,@body))
162+
```
163+
164+
The `',name` pattern (quote then unquote) inserts the literal symbol name into the output, so the log shows the actual function name.
165+
166+
## When to reach for a macro
167+
168+
Macros are powerful, but they are not always the right tool:
169+
170+
- **Use a macro** when you need to control evaluation order.
171+
- **Use a macro** when you want new syntax (like `time` or `with-output-buffer`).
172+
- **Use a macro** to eliminate boilerplate at compile time.
173+
- **Use a function** for everything else.
174+
175+
Functions are easier to debug, compose, and pass around. If a function can do the job, stick with it. Reach for macros when functions hit their limits.
176+
177+
## Debugging with macroexpand
178+
179+
When a macro misbehaves, `macroexpand` shows you the generated code without running it:
180+
181+
```phel
182+
(macroexpand '(unless (> x 10) (print "small")))
183+
# => (if (> x 10) nil (do (print "small")))
184+
```
185+
186+
Paste in your macro call, see what comes out. It takes the mystery out of debugging.
187+
188+
## Go build something
189+
190+
You now have the same metaprogramming tools that make Lisp so flexible. Start small: spot a pattern you repeat often and wrap it in a macro. Check the [macro documentation](/documentation/macros/) when you want to dig deeper.
191+
192+
Once you get comfortable, explore Phel's core. Even `defn` is just a macro that expands to `def` plus `fn`.
193+
194+
The `->` and `->>` threading macros, `cond`, `case`, `for` — they are all built with the same primitives you just learned. That is the Lisp way, and now it runs on PHP.

0 commit comments

Comments
 (0)