paseg
@behaviour, @callback and @spec
Hi
When I use @behaviour and @callback, the functions are defined. I guess that I do not need to use @spec for the implementation of the @impl functions as well?
Will Dialyzer sort this out as well?
Br Patrik
Most Liked
asummers
You don’t need them as Dialyzer will give a callback does not match spec error, but please include them anyway. As a reader I do not want to have to jump to the behaviour definition to find out what the arguments are. In that vein if you include a using macro where you define default callbacks that are overridable with defoverridable, elide the spec in the using macro, otherwise you’ll get a compiler error for duplicating the spec if anyone actually overrides and wants to spec the override.
YES
defmodule MyBehaviour do
@callback foo() :: :ok
end
defmodule MyImpl do
@impl MyBehaviour
@spec foo() :: :ok
def foo(), do: :ok
end
YES
defmodule MyBehaviour do
@callback foo() :: :ok
defmacro __using__(_) do
quote do
@impl MyBehaviour
@spec foo() :: :ok
def foo(), do: :ok
end
end
end
defmodule MyImpl do
use MyBehaviour
end
YES
defmodule MyBehaviour do
@callback foo() :: :ok
defmacro __using__(_) do
quote do
@impl MyBehaviour
def foo(), do: :ok
defoverridable [foo: 0]
end
end
end
defmodule MyImpl do
use MyBehaviour
@impl MyBehaviour
@spec foo() :: :ok
def foo(), do: :ok
end
NO
defmodule MyBehaviour do
@callback foo() :: :ok
defmacro __using__(_) do
quote do
@impl MyBehaviour
@spec foo() :: :ok
def foo(), do: :ok
defoverridable [foo: 0]
end
end
end
defmodule MyImpl do
use MyBehaviour
# this errors if you include the spec
# @spec foo() :: :ok
@impl MyBehaviour
def foo(), do: :ok
end
paseg
Hi
Wow, thanks for all comments! Did not know that this would stir up this many opinions. 
A see your point @asummers, but since this is not a public library (“only” used within our company), I prefer that the implementers spend the extra time to go into the definition of the behaviour rather than using multiple specs that will effect the maintenance in the long run.
I also found that the @spec may state less than the actual @callback without Dialyzer telling me, witch gives me another argument not to use the @specs…
Example given:
defmodule Register.DocEvents do
@callback initialize(soure :: binary() | atom()) :: :ok | {:error, String.t()}
end
defmodule Register do
@behaviour Register.DocEvents
@impl Register.DocEvents
@spec initialize(atom()) :: :ok
def initialize(source) do
...
end
end
Dialyzer signals this is ok, and I guess it is since the actual implementation fits within the original specification. In this case, the @spec makes sense since this implementation is not the same as the @callback stated, but if they are expected to be the same then adding an extra @spec just creates more maintenance burden.
Eiji
Sure, here is your changed code:
defmodule MyBehaviour do
@callback foo() :: :ok
end
defmodule MyImpl do
@behaviour MyBehaviour
@doc delegate_to: {MyBehaviour, :foo, 0}
@doc "Implementation-specific docs goes here …"
@impl MyBehaviour
def foo(), do: :ok
end
which would give:
iex(1)> h MyImpl.foo
def foo()
delegate_to: MyBehaviour.foo/0
Implementation-specific docs goes here …
iex(2)> b MyBehaviour.foo/0
@callback foo() :: :ok
This is much simpler than writing macros or copy-paste documentation and spec.
Generally we should avoid using macros unless it’s required.
asummers
I wasn’t suggesting to use a macro. Simply saying that if you do have a macro and the function is overridable to elide the spec in the macro (as they do in e.g. GenServer but do NOT elide in HTTPoison). I don’t think they’re incompatible, unless I’m misunderstanding something. This thread is about @spec not @doc.
asummers
Again, I’m not advocating for the macro, at all, in any way shape or form. I agree in this case it’s not needed. I’m contrasting the approaches taken between GenServer and HTTPoison.Base.
vs.
Dialyzer will pick this all up even if you drop all the @specs. But for readers of the code, giving them the ability to @spec the implementation is much more pleasant (even if they choose not to), because they do not need to look outside the file or inside IEx to be able to figure out what’s going on. And if they have the Credo rule on to require @spec for all functions, you must ignore in the implementation because the compiler will complain about duplicate specs for the implemented callbacks.







