🎓 A1Learner
🔍
0 of 46 lessons completed

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?