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?