Dan Gallagher
Dan Gallagher

Reputation: 1022

Implement a protocol with a behaviour in a macro in Elixir

I have a protocol that has a single function with a relatively complex API:

defprotocol Foo do
  def complex(foo, x, y)
end

I want to provide a way to implement this protocol for a common and simpler use case. After some experimentation, I came up with the following:

defmodule Bar do
  @callback simple(any, any) :: boolean

  defmacro __using__(_) do
    quote do
      @behaviour Bar

      defimpl Foo do
        # TODO this is really hacky. What is a better way to reference parent module?
        # Note that the 1 in drop(1) is from Foo's module name
        @parent_module __MODULE__ |> Module.split |> Enum.drop(1) |> Module.concat
        def complex(bar, x, _y) do
          matches = @parent_module.simple(bar, x)
          if matches, do: [x], else: []
        end
      end
    end
  end
end

Now I can implement Foo via use Bar, but the way that @parent_module is determined and used seems wrong. Is there a better way than using the hack in the TODO above? What is the idiomatic way to do this? I would rather not turn Foo into a behaviour because dispatch and consolidation of protocols fits the usage pattern.

Upvotes: 2

Views: 705

Answers (1)

Dogbert
Dogbert

Reputation: 222128

You can get and store the parent module's name in a local variable, then put it in an attribute in the implementation and then use it:

defmodule Bar do
  @callback simple(any, any) :: boolean

  defmacro __using__(_) do
    quote do
      @behaviour Bar

      parent_module = __MODULE__

      defimpl Foo do
        @parent_module parent_module

        def complex(bar, x, _y) do
          matches = @parent_module.simple(bar, x)
          if matches, do: [x], else: []
        end
      end
    end
  end
end

Upvotes: 4

Related Questions