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'
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.
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.
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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With