Skip to main content

Macros

On this page

Macros#

Macros are compile-time callables. They receive unevaluated code as data, transform it, and return new code for the compiler to process.

Why does this matter? In PHP, you cannot add new language constructs. Want unless (the opposite of if)? You are stuck with a function. Functions evaluate all arguments before the call, which breaks short-circuit logic and makes them second-class compared to if:

// PHP: forced to use closures to avoid premature evaluation
function unless(bool $cond, callable $then, callable $else): mixed {
    return $cond ? $else() : $then();
}

In Phel, a macro receives the raw code unevaluated, rewrites it, and the result compiles normally:

(defmacro unless [test then else]
  `(if (not ,test) ,then ,else))

(unless false "yes" "no")  ; => "yes"
;; Expands to: (if (not false) "yes" "no")
;; Only "yes" is ever evaluated. Behaves identically to a built-in if.

This works because Phel code is data. The call (unless false "yes" "no") is a plain Phel list, the same persistent list you work with everywhere. Macros manipulate that list at compile time using ordinary Phel functions.

defn, when, and, or, ->, ->> are all macros in Phel's standard library. They are not special compiler syntax. They are Phel code that rewrites other Phel code.

defn itself expands to def + fn:

(defn add [a b] (+ a b))
;; expands to:
(def add (fn [a b] (+ a b)))
PHP Coming from PHP?

PHP has no macro system. The common alternatives each have significant limitations:

  • eval() runs at runtime, has security implications, and cannot be type-checked or linted
  • Code generation produces files on disk, requires a build step, and the output is opaque
  • Attributes are metadata only. They cannot transform the code they annotate.

Phel macros run at compile time inside the compiler pipeline, produce normal Phel AST nodes, and are fully inspectable with macroexpand.

Quote#

quote returns its argument unevaluated. Single-quote prefix is shorthand for (quote form).

(quote my-sym) ; => my-sym
'my-sym ; same

Quote distinguishes code from data, making macros possible. Literals (numbers, strings) evaluate to themselves.

(quote 1) ; Evaluates to 1
(quote hi) ; Evaluates to the symbol hi
(quote quote) ; Evaluates to the symbol quote

'(1 2 3) ; Evaluates to the list (1 2 3)
'(print 1 2 3) ; Evaluates to the list (print 1 2 3). Nothing is printed.

Define a macro#

(defmacro name docstring? attributes? [params*] expr*)

defmacro creates a macro. Same params as defn.

With quote and defmacro, define a custom defn called mydefn:

(defmacro mydefn [name args & body]
  (list 'def name (apply list 'fn args body)))

Simple, doesn't cover all defn features, but shows the basics.

Quasiquote#

quasiquote improves macro readability. Inverts quoting: marks what should evaluate, leaves the rest unevaluated. Shorthand: ` (quasiquote), , (unquote), ,@ (unquote-splicing).

mydefn with quasiquote:

(defmacro mydefn [name args & body]
  `(def ,name (fn ,args ,@body)))
Clojure Coming from Clojure?

Quasiquote syntax works like Clojure:

  • ` for quasiquote (syntax-quote)
  • , for unquote
  • ,@ for unquote-splicing