Understanding the Tradeoffs with Elixir Typespecs and Dialyzer

In today’s post I want to start a conversation around the tradeoffs of using Typespecs and Dialyzer use in your Elixir code.

Elixir is a dynamic, not a statically-typed language – and just to preemptively avoid the programming wars that might ensue, the given type system of a language is not a “good” or “bad” attribute by itself, but it will none-the-less influences the code we write.

For those who prefer or come to Elixir from a statically-typed language background you may be interested in learning more about Elixir Typespecs, described in the documentation as follows:

Elixir comes with a notation for declaring types and specifications. Elixir is a dynamically typed language, and as such, type specifications are never used by the compiler to optimize or modify code. Still, using type specifications is useful because:

  • they provide documentation (for example, tools such as ExDoc show type specifications in the documentation)
  • they’re used by tools such as Dialyzer , that can analyze code with typespec to find type inconsistencies and possible bugs

While I have done some limited work with typespecs in my side projects, in my current client project the decree has been made we will define typespecs for all functions, both public and private, and we have dialyzer checks built into the CI to report any reported issues.

On the face of this requirement I do not have a problem, it is very well meaning towards building a stable codebase. Who can disagree with such a goal? The realties however of seeing it through day-to-day are more complex. First, let’s talk about how dialyzer errors can sometimes be hard to debug.

One recent error I got back from dialyzer was something like this:

Unknown type: Widget.t/0.

There was no function name, nor file name, nor line number to help me understand where this error was coming from. I was on my own. Luckily I knew the dialyzer check was clean before my I started my branch so I could at least look at my git diff for some ideas. This was not fun.

Another time I had a whole list of dialyzer errors reported back to me. Now if I was dealing with traditional complier errors I would work from the top of the list down, since sometimes when those first complier issues are resolved the later issues will disappear. Sadly when it comes to dialyzer errors that first error need not be what actually needs to get fixed. Many times you need to fix issues from the middle of the list first and knowing what to fix from that list is a learned art with its own dedicated learning curve.

Obtuse errors are bad enough but the second big issue for me is time. On a bigger project like ours it can sometime takes 10+ minutes to do a full dialyzer check the project. That long feedback loop can really slow down your day.

So where do we find the balance?

When it comes to Elixir code, my priorities of practices to enable a stable code base are as follows:

Of course every project has unique needs and tradeoffs, but this is where I land in the general case.

I’d love to hear your thoughts on typespecs and dialyzer.