DEV Community

loading...
Cover image for TIL: Understanding dialyzer’s “The pattern can never match the type.”

TIL: Understanding dialyzer’s “The pattern can never match the type.”

lasseebert profile image Lasse Skindstad Ebert ・3 min read

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:

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:

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:

@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:

@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.

Discussion (1)

pic
Editor guide
Collapse
vukanac profile image
Vladimir Vukanac

How does final b/0 look like?