Why does the compiler enforce that `@callback` definitions include type specs?

It seems like when you define a behaviour the compiler requires that you include type specs within the @callback definition. Then when you adopt the behaviour, the compiler warns if function_name/arity is not defined, but is perfectly happy if you don’t follow the type specs.

My question is, why does the compiler enforce that @callback definitions include type specs? I understand that Dialyzer will check these type specs for you, but that doesn’t seem like something the compiler should care about. Also, this means that anyone who wants to use behaviours to get compile-time function_name/arity checks has to buy-in to writing type specs, even if they otherwise want to use Elixir as a purely dynamic language.

For example:

If we omit the type specs, we get a compile error

defmodule Greeting do
  @callback hello(person)
end
# (CompileError) iex:82: type specification missing return type: hello(person)

To make the compiler happy, we have to include type specs:

defmodule Greeting do
  @callback hello(%Person{}) :: {:ok, String.t} | {:error, String.t}
end

Now when we adopt the behaviour, the compiler checks that function_name/arity is defined:

defmodule WesternGreeting do
  @behaviour Greeting
  def hello(), do: "Howdy"
end
# warning: undefined behaviour function hello/1 (for behaviour Greeting)

However all the type specs in the @callback are disregarded by the compiler:

defmodule WesternGreeting2 do
  @behaviour Greeting
  def hello([a, b, c]), do: a <> b <> c
end
# No warnings or errors
1 Like
defmodule Greeting do
  @callback hello(term) :: term
end

This would give you a “I don’t care” typespec. But this does communicate so much less information. The compiler would error for incorrect typespecs just as much as it does error about errors in @callbacks.

Adhereance to the typespecs is as everywhere else a dialyzer task and not one the compiler does do on it’s own.

Since generally the only thing callback declarations are, is documentation on the expected interface, I think it makes sense to document the types. Documenting only functions is not very useful to whoever is going to implement the behaviour. It is ultimately a choice, and I don’t think there are any deeper reasons behind it.

It is also possible to define behaviours using the old way, that does not require specs:

def behaviour_info(:callbacks), do: [foo: 1]

is similar to @callback foo(term) :: term.

1 Like