Using Dialyzer

Dialyzer analyzes code that runs on the Erlang VM, looking for potential errors. To use it with Elixir, we have to compile our source into .beam files and make sure that the debug_info compiler option is set (which it is when running mix in the default, development mode). Let’s see how to do that by creating a trivial project with two source files.

 $ ​​mix​​ ​​new​​ ​​simple
 ...
 $ ​​cd​​ ​​simple

Inside the project, let’s create a simple function. Being lazy, I haven’t implemented the body yet.

 defmodule​ Simple ​do
  @type atom_list :: list(atom)
  @spec count_atoms(atom_list) :: non_neg_integer
 def​ count_atoms(list) ​do
 # ...
 end
 end

Let’s run dialyzer on our code. To make life simple, we’ll use the dialyxir library to add a dialyzer task to mix:

typespecs/simple/mix.exs
 defp​ deps ​do
  [
  { ​:dialyxir​, ​"​​~> 0.5"​, ​only:​ [​:dev​], ​runtime:​ false }
  ]
 end

Fetch the library and build our project:

 $ ​​mix​​ ​​deps.get
 $ ​​mix​​ ​​compile

Now we’re ready to analyze our code. However, the first time we do this, dialyzer needs to construct a massive data structure containing all the types and APIs in both Erlang and Elixir. This lets it check not just our code, but also that our code is interacting correctly with the rest of the world. Building this data structure is slow: expect it to take 10 to 20 minutes! But once done, it won’t be repeated.

 mix dialyzer
 Compiling 2 files (.ex)
 warning: variable "list" is unused
  lib/simple.ex:8
 
 Generated simple app
 Checking PLT...
 [:compiler, :elixir, :kernel, :stdlib]
 Finding suitable PLTs
 Looking up modules in dialyxir_erlang-20.2.2_elixir-1.6.0-rc.0_deps-dev.plt
 . . .
 Checking 391 modules in dialyxir_erlang-20.2.2_elixir-1.6.0-rc.0_deps-dev.p
 lt
 Adding 48 modules to dialyxir_erlang-20.2.2_elixir-1.6.0-rc.0_deps-dev.plt
 
 Starting Dialyzer
 dialyzer args: [
  check_plt: false,
  init_plt: '/Users/dave/Work/Bookshelf/titles/elixir16/Book/code/typespecs
 /simple/_build/dev/dialyxir_erlang-20.2.2_elixir-1.6.0-rc.0_deps-dev.plt',
  files_rec: ['/Users/dave/Work/Bookshelf/titles/elixir16/Book/code/typespe
 cs/simple/_build/dev/lib/simple/ebin'],
  warnings: [:unknown]
 ]
 done in 0m1.18s
 lib/simple.ex:7: Invalid type specification for function
  'Elixir.Simple':count_atoms/1. The success typing is (_) -> 'nil'
 done (warnings were emitted)

Ouch! Let’s run it again:

 mix dialyzer --no-check
 Starting Dialyzer
 dialyzer args: [
 . . .
 ]
 done in 0m1.4s
 
 lib/simple.ex:7: Invalid type specification for function
  'Elixir.Simple':count_atoms/1. The success typing is (_) -> 'nil'
 done (warnings were emitted)

Those last three lines are the important ones. They’re complaining that the typespec for count_atoms doesn’t agree with the implementation. The success typing (think of this as the actual type)[43] returns nil, but the spec says it is a nonnegative integer. Dialyzer has caught our stubbed-out body.

Let’s fix that:

typespecs/simple/lib/simple.ex
 defmodule​ Simple ​do
  @type atom_list :: list(atom)
  @spec count_atoms(atom_list) :: non_neg_integer
 def​ count_atoms(list) ​do
  length list
 end
 end

and run dialyzer again:

 $ ​​mix​​ ​​dialyzer
 Compiling 1 file (.ex)
 Checking PLT...
 
 done in 0m1.34s
 done (passed successfully)

Let’s add a second module that calls our count_atoms function:

typespecs/simple/lib/simple/client.ex
 defmodule​ Client ​do
  @spec other_function() :: non_neg_integer
 def​ other_function ​do
  Simple.count_atoms [1, 2, 3]
 end
 end

Compile and dialyze:

 19∙18∙53≻ mix dialyzer
 Compiling 1 file (.ex)
 Generated simple app
 Checking PLT...
 done in 0m1.37s
 
 lib/simple/client.ex:3: Function other_function/0 has no local return
 lib/simple/client.ex:4: The call 'Elixir.Simple':count_atoms([1 | 2
  | 3,...]) breaks the contract (atom_list()) -> non_neg_integer()
 done (warnings were emitted)

That’s pretty cool. Dialyzer noticed that we called count_atoms with a list of integers, but it is specified to receive a list of atoms. It also decided this would raise an error, so the function would never return (that’s the no local return warning). Let’s fix that:

 defmodule​ Client ​do
  @spec other_function() :: non_neg_integer
 def​ other_function ​do
  Simple.count_atoms [​:a​, ​:b​, ​:c​]
 end
 end
 $ ​​mix​​ ​​dialyzer
 Compiling 1 file (.ex)
 
 done in 0m1.03s
 done (passed successfully)

And so it goes.…

Dialyzer and Type Inference

In this appendix, we’ve shown dialyzer working with type specs that we added to our functions. But it also does a credible job with unannotated code. This is because dialyzer knows the types of the built-in functions (remember the long wait the first time we ran it) and can infer (some of) your function types from this. Here’s a simple example:

 defmodule​ NoSpecs ​do
 def​ length_plus_n(list, n) ​do
  length(list) + n
 end
 def​ call_it ​do
  length_plus_n(2, 1)
 end
 end

Compile this, and run dialyzer:

 $ ​​mix​​ ​​dialyzer
 ...
 done in 0m1.28s
 
 lib/nospecs.ex:5: Function call_it/0 has no local return
 lib/nospecs.ex:6: The call 'Elixir.NoSpecs':length_plus_n(1,2) will
  never return since it differs in the 1st argument from the success
  typing arguments: ([any()],number())
 done (warnings were emitted)

Here it noticed that the length_plus_n function called length on its first parameter, and length requires a list as an argument. This means length_plus_n also needs a list argument, and so it complains.

What happens if we change the call to length_plus_n([:a, :b], :c)?

 defmodule​ NoSpecs ​do
 def​ length_plus_n(list, n) ​do
  length(list) + n
 end
 def​ call_it ​do
  length_plus_n([1, 2], ​:c​)
 end
 end
 $ ​​mix​​ ​​dialyzer
 done in 0m1.29s
 
 lib/nospecs.ex:5: Function call_it/0 has no local return
 lib/nospecs.ex:6: The call 'Elixir.NoSpecs':length_plus_n([1, 2],'c')
  will never return since it differs in the 2nd argument from the
  success typing arguments: ([any()],number())
 done (warnings were emitted)

This is even cooler. It knows that + (which is implemented as a function) takes two numeric arguments. When we pass an atom as the second parameter, dialyzer recognizes that this makes no sense, and complains. But look at the error. It isn’t complaining about the addition. Instead, it has assigned a default typespec to our function, based on its analysis of what we call inside that function.

This is success typing. Dialyzer attempts to infer the most permissive types that are compatible with the code—it assumes the code is correct until it finds a contradiction. This makes it a powerful tool, as it can make assumptions as it runs.

Does that mean you don’t need @spec attributes? That’s your call. Try it with and without. Often, adding a @spec will further constrain a function’s type signature. We saw this with our count_of_atoms function, where the spec made it explicit that we expected a list of atoms as an argument.

Ultimately, dialyzer is a tool, not a test of your coding chops. Use it as such, but don’t waste time adding specs to get a gold star.

Footnotes

[41]

http://www.erlang.org/doc/man/dialyzer.html

[42]

http://elixir-lang.org/getting-started/typespecs-and-behaviours.html#types-and-specs

[43]

http://www.it.uu.se/research/group/hipe/papers/succ_types.pdf