skateinmars

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

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

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

Where Next?

Popular in Questions Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New
Codball
Mix format works fine if run from the cmd. I’ve followed this to facilitate the implementation into VSC which involves downloading an ext...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Other popular topics Top

peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
AstonJ
You’re a programmer, so you don’t need spoon feeding with the conventional drivel about “this is an integer.” No. You need to know what’s...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement