0
votes

I have a project where I need to define a number of protocols with different names but having totally the same defines.

I've tried to do it with use/2 but no luck: it doesn't work, defines from __using__/1 don't appear in the resulting protocol:

defmodule Action do
  defmacro __using__(_) do
    quote do
      def run(tool)
    end
  end
end

defprotocol Actions.ShowId do
  use Action
end

defmodule Tools.SimpleRelay do
  defstruct [:id]

  defimpl Actions.ShowId do
    def run(%Tools.SimpleRelay{id: id}), do: IO.puts(inspect(id))
  end
end

On the struct with protocol implementation definition I get:

warning: module Actions.ShowId is not a behaviour (in module Actions.ShowId.Tools.SimpleRelay)
  iex:8: Actions.ShowId.Tools.SimpleRelay (module)

And if I try to use it:

iex(6)> relay = struct(Tools.SimpleRelay, id: "ALongStringId")
%Tools.SimpleRelay{id: "ALongStringId"}
iex(7)> Actions.ShowId.run(relay)
** (UndefinedFunctionError) function Actions.ShowId.run/1 is undefined or private
    Actions.ShowId.run(%Tools.SimpleRelay{id: "ALongStringId"})

Is it a bug or a purpose? Or just my incorrect definition?

I've tried to ask the same question on elixirforum (and posted there this task background) and the only suggestion I got so far is that it is a bug. I would like to clarify it is really a bug before opening an issue.

Thanks!

1

1 Answers

0
votes

Kernel.defprotocol/2 delegates to Protocol.__protocol__/2 which in turn redefines def/2 by unexporting Kernel.def/2 and importing Protocol.def/1 instead. In the context of your macro, def macro is referring to imported ATM Kernel.def/2, which breaks the desired behaviour.

I am not sure if this is a bug (error message could have been way more descriptive, though,) but you might easily overcome it by calling Protocol.def/1 explicitly, using FQN.

defmodule Action do
  defmacro __using__(_) do
    quote do
    # ⇓⇓⇓⇓⇓⇓⇓⇓  HERE
      Protocol.def(run(tool))
    end
  end
end

defprotocol Actions.ShowId do
  use Action
end

defmodule Tools.SimpleRelay do
  defstruct [:id]

  defimpl Actions.ShowId do
    def run(%Tools.SimpleRelay{id: id}), do: IO.puts(inspect(id))
  end
end

If you dislike how formatter handles parentheses around Protocol.def/1, feel free to delegate it to some local defp/1 or something and amend your .formatter.exs with

[
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
  locals_without_parens: [:defp]
]