Functional languages rely heavily on types to determine how functions interact with each other, Elixir is no exception. You can significantly improve the mission statement of a function with typespecs. Typespec logs the expected input and output of a function. A type specification is not required, but can be useful because it requires the developer to explicitly declare what a function accepts and what it returns.
To define a typespec, you use the
@spec
module directive corresponding with the function that you want to describe. Once this directive is written, you must then write out the function that you’re describing.
def module Math do@spec add(integer(), integer()) :: integer()def add(x, y) dox + yendend
Elixir is a dynamically typed language, which means that you don’t have to declare the type of variables or functions. The programming language automatically infers types based on function signatures. Due to this, Elixir doesn’t need type declarations because the language does it for you. The compiler only cares if the number of function arguments, or
Did you know that Elixir ships with Dialyzer, a tool that helps detect mistakes early in the coding process? With just a few annotations on function definitions, Dialyzer can provide great benefits. It also works with Elixir projects in general, but it’s most effective in projects where typespecs are used in function definitions.
Jeremy Huffman has written a library called Dialyxir, which is a set of easy-to-use mix
tasks for Dialyzer, an Erlang tool named from the characters in DI
screpancy A
naLYZ
er for ER
lang. Firstly, Dialyxir needs to be added as a dependency in mix.exs
:
defp deps do[{:dialyxir, "~> 0.5.1", only: [:dev], runtime: false}]end
Now let’s configure it by updating the project
function in mix.exs
.
def project do [ app: :project_name, dialyzer: [plt_add_deps: :transitive],
The Persistent Lookup Table, or PLT, is a compiled cache containing the analysis of your application. Without it, running Dialyxir would take ages.
First run $ mix dialyzer
and then wait for it to run. You’ll eventually need to cancel it. Now you understand why building this
In short, if you will run
$ mix dialyzer
for the first time, it will take at least 10 - 15 minutes. However, when you run it again, it won’t take long. This is because now we have our cached dialyzer results in the PLT.
Let’s create an example application which will “hexify” library names by appending _ex to the given string, unless one already exists. The main .ex
file will be as follows:
defmodule BeliefStructure dodef hexify(package) docase String.ends_with?(package, "ex") dotrue -> packagefalse -> BeliefStructure.Hexify.name(package)endendend
Let’s write some specifications that look correct, but are actually incorrect. This way, we can test Dialyxir to see what it can find for us.
defmodule BeliefStructure.Hexify do@spec name(integer) :: integerdef name(package) dopackage(package)end@spec package(boolean) :: booleandef package(package) dopackage <> "_ex"endend
Now run $ mix dialyzer
and wait for some time to run at first. You’ll clearly see what’s broken:
the invalid type specification warning shows that both name/1 and package/1 expect a binary. You can infer both from the code, but Dialyxir makes it explicit.
Now fix the specs and return Dialyxir, like this:
defmodule BeliefStructure.Hexify do
@spec name(String.t) :: String.t
def name(package) do
package(package )
end
@spec package(String.t) :: String.t
defp package( package ) do
package <> "_ex"
end
end
- The function
handle_cast/2
has no local return.- The return type
tuple()
in the specification of init/1 is not a subtype of ‘ignore’ | {‘ok’,} | {‘stop’,} | {‘ok’,_,‘hibernate’} | {‘infinity’} | {non_neg_integer()}, which is the expected return type for the callback ofElixir.GenServer
behavior.
Both of them are easily solved. For the no local return warning, you must explicitly declare that the handle_cast/2
function will fail by adding no_return()
as the return type of its @spec
. The second warning happens when the return type of your @spec
does not match the return type defined for the callback by Elixir’s GenServer.
So, while it can be helpful to add in typespecs to guard you from some mistakes, it may be overwhelming to add them retroactively or along the way. Dialyzer is one of those tools that can be difficult to understand. It requires an explicit step to run, and produces cryptic errors. It’s an acquired taste that tends to be popular among the Erlang community at large.
Free Resources