Elixir Hygienic DSL
A hygienic DSL is a small language inside Elixir whose macro variables stay separate from the caller's variables. It lets callers write short route lines while the macro keeps its own variables apart.
Hygiene keeps caller variables apart
The caller keeps 1 while the macro result is 99. The macro variable never overwrites the caller variable.
defmodule Hyg do
defmacro set_x do
quote do
x = 99
x
end
end
end
defmodule Caller do
require Hyg
def run do
x = 1
result = Hyg.set_x()
{x, result}
end
end
IO.inspect(Caller.run())var! can write a caller variable on purpose
The caller saves 1 before the macro runs. After the macro, the same variable holds 99.
defmodule Breaker do
defmacro set_x do
quote do
var!(x) = 99
end
end
end
defmodule Caller do
require Breaker
def run do
x = 1
before = x
Breaker.set_x()
{before, x}
end
end
IO.inspect(Caller.run())var! can read a caller variable
The caller binds 5. The macro reads that same variable through var!.
defmodule Breaker do
defmacro read_x do
quote do
var!(x)
end
end
end
defmodule Caller do
require Breaker
def read do
x = 5
Breaker.read_x()
end
end
IO.puts(Caller.read())Example
defmodule Router do
defmacro __before_compile__(env) do
routes = Module.get_attribute(env.module, :routes) |> Enum.reverse()
quote do
def routes(), do: unquote(Macro.escape(routes))
end
end
defmacro route(method, path, handler) do
if method not in [:get, :post] do
raise ArgumentError, message: "unknown method"
end
if not is_binary(path) or not String.starts_with?(path, "/") do
raise ArgumentError, message: "bad path"
end
tuple = {method, path, handler}
quote do
@routes unquote(Macro.escape(tuple))
end
end
end
defmodule Caller do
@before_compile Router
Module.register_attribute(__MODULE__, :routes, accumulate: true)
require Router
Router.route(:get, "/home", :home)
Router.route(:post, "/save", :save)
end
IO.inspect(Caller.routes())Key points
- Hygiene: hygiene is the rule that a variable made inside a macro does not clash with a same-named caller variable.
- Var: var! marks a variable as belonging to the caller, so a macro can read or write it on purpose.
- Caller variable: a caller variable is a variable bound in the module that uses the macro.
- Route DSL: a route DSL lets a caller write short lines that each name a method, a path, and a handler.
- Escape: Macro.escape keeps a three-part route whole when a macro inserts it into code.
In this example, each route line is checked and gathered, and the hook turns the gathered routes into one list function in written order.
Note
breaking hygiene on purpose is rare, so use var! only when the caller variable is part of the design.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
What does var! do to a variable?