Pete
Pete

Reputation: 2246

How to use typespecs and Dialyzer with Behaviours?

In Elixir how can I document that a function will return a module that implements a specific behaviour?

To use a trivial example, say I have created a GreeterBehaviour behaviour that is implemented by two modules:

defmodule GreeterBehaviour do
  @callback say_hello(String.t) :: String.t
end

defmodule FormalGreeter do
  @behaviour GreeterBehaviour

  def say_hello(name) do
    "Good day to you #{name}"
  end
end

defmodule CasualGreeter do
  @behaviour GreeterBehaviour

  def say_hello(name) do
    "Hey #{name}"
  end
end

I then want to easily swap out either of those implementations by retrieving the Greeter via a function:

defmodule MyApp do
  def main do
    greeter().say_hello("Pete") |> IO.puts
  end

  @spec greeter() :: GreeterBehaviour # This doesn't work with dialyzer
  def greeter do
    FormalGreeter # Can easily be swapped to CasualGreeter
  end
end

Dialyzer will successfully check that both CasualGreeter and FormalGreeter correctly implement the GreeterBehaviour behaviour. However, how can I define a typespec so that Dialyzer will check that greeter/0 returns a module that does in fact implement GreeterBehaviour?

Using @spec greeter() :: GreeterBehaviour doesn't work as Dialyzer will throw a warning:

lib/my_app.ex:19: Invalid type specification for function 'Elixir.MyApp':greeter/0. The success typing is () -> 'Elixir.FormalGreeter'

Upvotes: 16

Views: 1382

Answers (1)

Anderson Cook
Anderson Cook

Reputation: 64

In your behaviour you could define a type for say_hello:

@type f :: (String.t() -> String.t())

Your greeter function could return a module + function: &module.say_hello/1

And the spec would be:

@spec greeter() :: GreeterBehaviour.f()
defmodule GreeterBehaviour do
  @type f :: (String.t() -> String.t())
  @callback say_hello(String.t()) :: String.t()
end

defmodule FormalGreeter do
  @behaviour GreeterBehaviour

  def say_hello(name) do
    "Good day to you #{name}"
  end
end

defmodule CasualGreeter do
  @behaviour GreeterBehaviour

  def say_hello(name) do
    "Hey #{name}"
  end
end

defmodule MyApp do
  def main do
    greeter().("Pete") |> IO.puts()
  end

  # This will work with dialyzer
  @spec greeter() :: GreeterBehaviour.f()
  def greeter do
    # Can easily be swapped to CasualGreeter
    &FormalGreeter.say_hello/1
  end
end

Upvotes: 0

Related Questions