Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use typespecs and Dialyzer with Behaviours?

Tags:

elixir

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'
like image 988
Pete Avatar asked Jun 24 '17 01:06

Pete


People also ask

What does@ spec do Elixir?

For this purpose Elixir has @spec annotation to describe the specification of a function that will be checked by compiler. However in some cases specification is going to be quite big and complicated. If you would like to reduce complexity, you want to introduce a custom type definition.

Is Elixir dynamic?

Elixir is a dynamically typed language, so all types in Elixir are checked at runtime. Nonetheless, Elixir comes with typespecs, which are a notation used for: declaring typed function signatures (also called specifications); declaring custom types.

What is a Typespec?

To help with figuring out which types a function is designed to work with, Elixir provides something called a type specification. A type specification (also called a "typespec") is a special notation that allows the developer to document which types a function expects to receive and return.

What is dialyzer Elixir?

Dialyzer is a static analysis tool for Erlang and other languages that compile to BEAM bytecode for the Erlang VM. It can analyze the BEAM files and provide warnings about problems in your code including type mismatches and other issues that are commonly detected by static language compilers.


1 Answers

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
like image 163
Anderson Cook Avatar answered Oct 22 '22 23:10

Anderson Cook