What is Dialyxir and its demonstration?

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) do
x + y
end
end

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 arityArity is the number of augments or operands taken by a function or operation and function names match.

Dialyzer is a tool to do static analysis of elixir code.

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 DIscrepancy AnaLYZer for ERlang. 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 cachePersistent Lookup Table is important.

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 do
def hexify(package) do
case String.ends_with?(package, "ex") do
true -> package
false -> BeliefStructure.Hexify.name(package)
end
end
end

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) :: integer
def name(package) do
package(package)
end
@spec package(boolean) :: boolean
def package(package) do
package <> "_ex"
end
end

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

  1. The function handle_cast/2 has no local return.
  2. 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 of Elixir.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

Copyright ©2025 Educative, Inc. All rights reserved