I’ve encountered some unusual behavior from Dialyzer while validating values from Application.get_env and reduced it down to the following code with a deliberately incorrect typespec (should be list(module()) on the return):
@spec actually_a_list_of_modules(list(term())) :: list(DateTime.t())
def actually_a_list_of_modules(list) when is_list(list) do
validate_modules(list)
list
end
defp validate_modules(modules), do: Enum.each(modules, &ensure_valid_module/1)
defp ensure_valid_module(a) when is_atom(a), do: :ok
defp ensure_valid_module(a), do: raise("Not a module: #{inspect(a)}")
Dialyzer accepts this without complaint, and I’m not sure why. When I change the validate_modules function to destructure the list manually, Dialyzer complains as expected:
# This implementation fixes the problem, and Dialyzer correctly shows an error.
defp validate_modules([]), do: nil
defp validate_modules([a | b]) do
ensure_valid_module(a)
validate_modules(b)
end
I also tried implementing this with a comprehension to see if something specific to Enum.each was affecting the type checking, but it behaves in the same way as the first example.
# This is incorrectly accepted, the same as the Enum.each version.
defp validate_modules(modules), do: for(m <- modules, do: ensure_valid_module(m))
I understand this could just be a limitation of Dialyzer, and I’m happy to refactor my code accordingly. I’d love to know what’s causing this behaviour, though. Why’s Dialyzer getting confused?
(Reproduced on Elixir 1.16.3 + Erlang/OTP 26.2 and also Elixir 1.14.3 + Erlang/OTP 25.3)




















