🎓 A1Learner
🔍
0 of 46 lessons completed

Elixir DSLs with Macros

A DSL is a small readable syntax for one narrow job. Macros build that syntax, turning friendly lines into ordinary data.

One friendly line becomes data

The item macro turns one line into a pair.

defmodule TaskDSL do
  defmacro item(name) do
    quote do: {unquote(name), :todo}
  end
end
defmodule TaskCaller do
  require TaskDSL
  def run do
    TaskDSL.item("Pack")
  end
end
IO.inspect(TaskCaller.run())

A block becomes a list

The checklist macro reads every item line and returns their names in order.

defmodule Checklist do
  defmacro checklist(do: block) do
    calls =
      case block do
        {:__block__, _, calls} -> calls
        single -> [single]
      end
    names = Enum.map(calls, fn {:item, _, [name]} -> name end)
    quote do: unquote(names)
  end
end
defmodule ChecklistCaller do
  require Checklist
  def run do
    Checklist.checklist do
      item "Pack"
      item "Lock"
      item "Go"
    end
  end
end
IO.inspect(ChecklistCaller.run())

A route list reads like a list

Each get line becomes a route tuple with a method and a path.

defmodule MiniRouter do
  defmacro routes(do: block) do
    calls =
      case block do
        {:__block__, _, calls} -> calls
        single -> [single]
      end
    pairs =
      Enum.map(calls, fn
        {:get, _, [path, handler]} -> {:get, path, handler}
        {:post, _, [path, handler]} -> {:post, path, handler}
      end)
    quote do: unquote(Macro.escape(pairs))
  end
end
defmodule RouteCaller do
  require MiniRouter
  def run do
    MiniRouter.routes do
      get "/home", :home_page
      get "/about", :about_page
      post "/login", :login_page
    end
  end
end
routes = RouteCaller.run()
IO.puts(length(routes))
IO.inspect(Enum.at(routes, 0))

Example

defmodule MiniRouter do
  defmacro routes(do: block) do
    calls =
      case block do
        {:__block__, _, calls} -> calls
        single -> [single]
      end
    pairs =
      Enum.map(calls, fn
        {:get, _, [path, handler]} -> {:get, path, handler}
        {:post, _, [path, handler]} -> {:post, path, handler}
      end)
    quote do: unquote(Macro.escape(pairs))
  end
end
defmodule RouteCaller do
  require MiniRouter
  def run do
    MiniRouter.routes do
      get "/home", :home_page
      get "/about", :about_page
      post "/login", :login_page
    end
  end
end
routes = RouteCaller.run()
IO.puts(length(routes))
found = Enum.find(routes, fn {_method, path, _handler} -> path == "/about" end)
IO.inspect(found)

Key points

  • DSL: a DSL is a small syntax shaped for one narrow job.
  • Readability: DSL lines read like a list, not like code for building data.
  • Macro: a macro turns each DSL line into ordinary data.
  • Block: a macro that takes a do block can read all its lines at once.
  • Use Macro.escape to keep a built list of tuples safe for unquote.

In this example, the routes macro reads three friendly lines into a list of route tuples, the list length is printed, and Enum.find picks out the route for the about path.

Note

a DSL stays narrow, so it does not replace ordinary code for other jobs.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

What is a DSL for?