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.