How to dynamically create behaviours?

I want to have this module…

defmodule Foo do
  use DynamicBehaviour
  
  @functions [:foo, :bar]
end

Expand to this…

defmodule Foo do
  
  defmodule Behaviour do
    @callback foo(term) :: :ok | {:error, term}
    @callback bar(term) :: :ok | {:error, term}
  end
  
  @behaviour Foo.Behaviour
  
end

And I can’t get the metaprogramming quite right. This is what I have so far…

defmodule DynamicBehaviour do

  defmacro __using__(_opts \\ []) do
    quote do
      @before_compile DynamicBehaviour
    end
  end
  
  defmacro __before_compile__(_env) do
    quote do

      defmodule Behaviour do
        Enum.each(@functions, fn function ->
          @callback unquote(function)(term) :: :ok | {:error, | term}
        end)
      end

      @behaviour Behaviour
    end
  end
  
end

But it’s not working. I understand that @functions does not exist in Foo.Behaviour, but in only Foo, but I’ve tried different things and still can’t get it to work. I kind of feel like it’s related to some kind of “double quoted” issue.

Thanks for the help.

1 Like

You either need to have @functions before the use, or instead use arguments to the use DynamicBehaviour, functions: ~w[a b c]

1 Like

I thought that @before_compile addresses that exact issue.

1 Like

Not really, before compile does not simply append IIRC. Also, the functions you use there already uses its own @functions, as it’s inside a new module.

I’m not sure 100%, but perhaps this works:

quote do
  functions = @functions
  defmodule B do
    Enum.each(functions, ...)
  end
end
1 Like

Even if I use a list literal, it still doesn’t work…

defmodule Behaviour do
  Enum.each([:foo, :bar], fn function ->
    @callback unquote(function)(term) :: :ok | {:error, | term}
  end)
end

The error is on the @callback line and says:

variable "function" does not exist and is being expanded to "function()"

It kinda feels because I’m inside a quote, do: already, so the unquote is working on the outer scope instead of inside the each/defmodule. If that makes sense.

In other words… this works…

defmodule Foobar do
  Enum.each([:foo, :bar], fn function ->
    @callback unquote(function)(term) :: :ok | {:error, | term}
  end)
end

But this doesn’t work…

quote do
  defmodule Foobar do
    Enum.each([:foo, :bar], fn function ->
      @callback unquote(function)(term) :: :ok | {:error, | term}
    end)
  end
end

Figured it out…

defmodule Foo do

  Enum.each([:foo, :bar], fn name ->
    @callback unquote(name)(term) :: :ok
  end)

  defmacro define_callbacks do
    quote bind_quoted: [] do
      Enum.each([:foo, :bar], fn name ->
        @callback unquote(name)(term) :: :ok
      end)
    end
  end

end


defmodule Bar do
  @behaviour Foo

  def foo("blah"), do: :ok
  def bar("blah"), do: :ok
end

require Foo

defmodule Foo2 do
  Foo.define_callbacks()
end

defmodule Bar2 do
  @behaviour Foo2

  def foo("blah"), do: :ok
  def bar("blah"), do: :ok
end

The :bind_quoted option on quote did the trick, as explained by the official docs… :slight_smile:
https://hexdocs.pm/elixir/Kernel.SpecialForms.html#quote/2-binding-and-unquote-fragments

3 Likes