Good morning everyone!
I’ve stumbled in a very strange, and probably trivial, error with dialyzer in a simple function, e.g.
defmodule DialyzerFp do
@spec has_put?(module()) :: true | no_return()
def has_put?(mod) do
with {:ensure, {:module, _}} <- {:ensure, Code.ensure_loaded(mod)},
{:implements, true} <-
{:implements, function_exported?(mod, :put, 3)} do
true
else
{:ensure, error} ->
raise "Module [#{inspect(mod)}] could not be loaded [#{inspect(error)}]"
{:implements, _} ->
raise "Module [#{inspect(mod)}] does not export put/3"
end
end
end
Calling mix.dialyzer on a project consisting of this module alone yields to
lib/dialyzer_fp.ex:9:pattern_match
The pattern can never match the type.
Pattern:
{:ensure, _error}
Type:
{:implements, false}
Fun part is that if I invert the branches in else, dialyzer inverts the error like
lib/dialyzer_fp.ex:10:pattern_match
The pattern can never match the type.
Pattern:
{:implements, _}
Type:
{:ensure, {:error, :badfile | :embedded | :nofile | :on_load_failure}}
Consider also:
the function is working as expected in all 3 cases (both this and the original one)
tried without typespecs
tried using atom, atom | module, any, is_atom guard
Does anyone have any hint on what could be going on?
Thanks, as always!
I know it’s annoying especially if you require dialyzer pass in your CI.
Though, not the direct answer to the question, but I’d like to point out that pattern used in the example (tagging each expressions with an atom in a tuple to differentiate in “else”) is discouraged. (see here)
And would be better to take out each tagged expression into its own small function:
defmodule DialyzerFp do
@spec has_put?(module()) :: true | no_return()
def has_put?(mod) do
with :ok <- ensure_code_loaded(mod) do
implements_put_3?(mod)
end
end
defp ensure_code_loaded(mod) do
case Code.ensure_loaded(mod) do
{:module, _} -> :ok
error -> raise "Module [#{inspect(mod)}] could not be loaded [#{inspect(error)}]"
end
end
defp implements_put_3?(mod) do
with false <- function_exported?(mod, :put, 3) do
raise "Module [#{inspect(mod)}] does not export put/3"
end
end
end
And with that code dialyzer shouldn’t get confused =)
Thanks for the hints!
You’re totally right in underlining how this is a discouraged practice (I personally allow “small else branches” in my code because I work well in seeing every outcome of certain functions in a single place and I don’t like to specialize otherwise generic functions in returning a certain form of output, but that’s on me and I can agree with the general concept)
Also, you’re right in saying that dialyzer is happier in dividing in different functions (fun enough, I have in the meanwhile solved this way but in the discourage errors handling part, shame on me )
This solves the practical issue, but it bugs me not having understood the reason behind this behaviour