skateinmars
Accessing behaviour information of a module at compile time
Hello, I work with a team of Elixir devs and some time ago we came across a code pattern within the Hex.pm website source that has an elegant solution for configuring which implementation is used within the behaviour: https://github.com/hexpm/hexpm/blob/master/lib/hexpm/billing/billing.ex
This solution fits the “explicit contracts” philosophy and works great, since you can for instance simply call Hexpm.Billing.get(organization) in a controller and not have to know about using Application.get_env(:hexpm, :billing_impl) everywhere.
As you can see from the code though, you need to write a definition for each callback function, that will simply delegate to impl().
I tried to automate some of that work using metaprogramming, but I hit a roadblock: I have no way of accessing the callback attributes during compilation!
Here is my sample module:
defmodule Foobar do
@moduledoc """
Behaviour for Foobar.
"""
@doc """
Reticulates the spline.
"""
@callback reticulate() :: :ok | {:error, any()}
defp impl(),
do: Application.get_env(:myapp, :foobar_impl, Foobar.InMemory)
# I want this to be autogenerated:
# def reticulate(), do: impl().reticulate()
end
defmodule Foobar.InMemory do
@behaviour Foobar
@impl true
def reticulate do
:ok
end
end
I am able to use the behaviour_info function once the module is compiled:
Foobar.behaviour_info(:callbacks) |> IO.inspect(label: "callbacks list after compilation")
# => callbacks list after compilation: [reticulate: 0]
But this function is not yet accessible from the module while I’m trying to write a macro.
My other solution would have been to access the @callback attributes directly, but it seems that typespecs attributes have a special meaning to the compiler, and I cannot access them:
defmodule Implementor do
defmacro list_callbacks() do
quote do
Module.get_attribute(__MODULE__, :callback)
|> IO.inspect(label: "callback attribute")
Module.get_attribute(__MODULE__, :notcallback)
|> IO.inspect(label: "notcallback attribute")
end
end
end
defmodule Foobar do
require Implementor
@callback reticulate() :: :ok | {:error, any()}
@notcallback "reticulate"
Implementor.list_callbacks()
end
# => callback attribute: nil
# => notcallback attribute: "reticulate"
Is there any solution I may have missed?
As a workaround, I thought about generating a second module (Foobar.Impl) using an after_compile callback, but I found the solution of putting behaviour and implementation in the same module a bit more elegant.
Marked As Solved
jola
It’s because you put the two modules in the same file. I split it into two and
➜ app mix compile --force
Compiling 3 files (.ex)
callback attribute: [
{:callback,
{:::, [line: 4],
[
{:reticulate, [line: 4], []},
{:|, [line: 4], [:ok, {:error, {:any, [line: 4], []}}]}
]}, {Foobar, {4, 1}}}
]
notcallback attribute: "reticulate"
Also Liked
skateinmars
Thanks, that did the trick!
Macros are still mostly magic for me but I managed to write a usable module. Here is the end result:
defmodule ImplGenerator do
defmacro __using__(_) do
quote location: :keep do
@before_compile {ImplGenerator, :__generate_impl__}
end
end
defmacro __generate_impl__(_env) do
quote do
callbacks = Module.get_attribute(__MODULE__, :callback)
Enum.each(callbacks, fn {:callback, {:::, _, [{fname, _, args_cb_ast}, _]}, _} ->
arity = length(args_cb_ast)
args = Macro.generate_arguments(arity, __MODULE__)
ast =
quote do
def unquote(fname)(unquote_splicing(args)) do
impl().unquote(fname)(unquote_splicing(args))
end
end
Code.eval_quoted(ast, [], __ENV__)
end)
end
end
end







