28 Days - The Beauty of Macros
I’m aiming for a shorter post today, as I presented a 50 minute talk about “Elixir in Production” at the Atlanta Elixir Meetup tonight. I’m proud of how the talk turned out, but I’m also looking forward to hearing what content will make it better, and submitting it to conferences.
For the end of the first week (7th day) of Elixir writing, I wanted to break down a small chunk of code that I recently wrote that utilizes macros. In particular, there’s a cool property of macros that I want to explain. Let’s just go into the code:
defmodule SalesloftApi.Phoenix.Router do
defmacro setup({:__aliases__, _ctx, module_path}, controller) do
router_module = Module.concat(module_path)
router_module.min_api_version()..router_module.current_api_version()
|> Enum.flat_map(fn api_version ->
router_module.routes()
|> Map.values()
|> Enum.map(fn route = %{http_verb: verb} ->
path = SalesloftApi.Route.path(route)
quote do
scope "/v#{unquote(api_version)}" do
unquote(verb)(unquote(path), unquote(controller), :execute, assigns: %{route: unquote(Macro.escape(route))})
end
end
end)
end)
end
end
For context, this code is an adapter to convert between a router module and phoenix routes. We are doing this in order to provide our API at a level outside of just HTTP (like websockets or console) and to ensure that we can control how the API is documented at a very native level. I previous wrote about a similar concept in Ruby.
This code is a macro which accepts a module and a controller module into it. It’s intended to be called from a Phoenix.Router module.
The code follows this simplified flow:
- Iterate over available versions of the API (2->2 currently)
- Iterate over the available routes
- Generate some Phoenix.Router compatible code like:
scope "/v2" do
get("/widgets", FrontController, :execute, assigns: %{route: route})
end
- Provide the generated code as the macro’s return.
Here is my favorite part of this code: it doesn’t know what phoenix is. This project
does not take phoenix in as a dependency…you don’t need to use phoenix to use this module.
What needs to happen is that there must be code that supports scope
and get
/friends in
the module that calls this. It just happens to be phoenix.
This captures the flexibility of Elixir very concisely. The code that a macro generates is “placed into” the call site, and the original library doesn’t execute that code.
If you do decide to learn what macros are about, I can’t recommend Metaprogramming Elixir enough. I found the book to be excellent at highlighting how macros are really used in the Elixir code that I use daily.
Thanks for reading the 7th post in my 28 days of Elixir. Keep up through the month of February to see if I can stand subjecting myself to 28 days of straight writing. I am leaving for vacation tomorrow. I’m excited for the time to reflect on interesting topics and to have more time in the evening to write about them…hopefully. Cheers!