TIL: Understanding dialyzer’s “The pattern can never match the type.”
Tags: elixir, dialyzer
Published: 2019-10-20 19:49:52

*This story was originally posted by me on Medium on 2019-09-03, but moved here since I'm closing my Medium account.*

I like dialyzer and typespecs. They are a natural part of my TDD flow:

* Write a failing test
* Write docs for function
* Write typespecs for function
* Write the function
* See that test is now green
* Refactor
* See that test is still green

Having written tests, docs and a spec for a function, makes it much more clear what the function should accept as arguments and what it should return. I also tend to write more reusable functions with this flow.

The typespecs will also function as documentation when reading the function.

But dialyzer has a downside. Debugging dialyzer warnings can be a real pain and it is often not clear where to look for a wrong typespec or whatever made dialyzer vomit all over my terminal with less-than-obvious messages.

I spend a good three hours today debugging a dialyzer warning in my Elixir app, so I’m naturally obligated to share my insights with the world :)

## The setup

I got an error in a with statement much like this one in function `c/0`:

```elixir
defmodule DialyzerTest do
  @spec a() :: :ok | :error
  def a do
    [:ok, :error] |> Enum.random()
  end

  @spec b() :: :ok | :another_error
  def b do
    [:ok, :another_error] |> Enum.random()
  end

  @spec c() :: :ok | :error
  def c do
    with :ok <- a(),
         :ok <- b() do
      :ok
    else
      :error -> :error
    end
  end
end
```

The dializer warning says:

```
simple_dialyzer_test.ex:14:pattern_match
The pattern can never match the type.

Pattern:
:error

Type:
:another_error
```

This is a pretty light-weight warning compared to dialyzer standards. Normally I need to copy-paste the message into an editor to format it and be able to make a little sense out of it.

So apparently `:error` does not match `:another_error`, which is obvious, but why complain about it? When I wrote the code, I called `b/0` in a way that shouldn’t make it return anything else than `:ok`, so I would like a runtime exception if anything else is returned from that function.

My mistake was that I considered the above example the same as this one:

```elixir
defmodule DialyzerTest do
  @spec a() :: :ok | :error | :another_error
  def a do
    [:ok, :error, :another_error] |> Enum.random()
  end

  @spec c() :: :ok | :error
  def c do
    with :ok <- a() do
      :ok
    else
      :error -> :error
    end
  end
end
```

Notice that we only match on `:ok` and `:error`, but not on `:another_error`. This code does not produce a dialyzer warning.

What the hell is going on?

After a long time of debugging and trying to find a wrong typespec, I realized how dialyzer handles with: For each statement in the `with` (all the lines with `a <- b`), we can think of it as “`b` must either match `a` or must match at least one clause in `else`”.

I tried digging deeper in the elixir source code to get a confirmation of this, but `with` is a special form and implemented in dark magic, so I got no further.

## Solution?

Yes, there is an easy solution. If you don’t want the statement in the `with` to match anything in `else`, don’t include the statement in the `with`. You can include it before, after or inside `do`, whateever fits the use case.

In my small example from the top, I would simply rewrite, so that the call to `b/0` was moved inside the `do` and matched with the expected return value:

```elixir
@spec c() :: :ok | :error
def c do
  with :ok <- a() do
    :ok = b()
    :ok
  else
    :error -> :error
  end
end
```

And since I now only have a single statement in the `with`, I would probably refactor to a `case`:

```elixir
@spec c() :: :ok | :error
def c do
  case a() do
    :ok ->
      :ok = b()
      :ok

    :error ->
      :error
  end
end
```

Again, dialyzer was right. It does not make sense to have a statement inside a `with` if I’m never going to match a clause in `else` with it. My code is now easier to read, since it is obvious to the reader that `b/0` _must_ return `:ok`.