|
| 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