This is correct. dialyzer does not unify type variables. Type variables are only really useful for specifying constraints via a when clause, and do not provide most of what people expect generics to do for them.
This is mentioned in this answer by one of the developers of dialyzer.
Came across this discussion when searching for Elixir types / dialyzer stuff. I think itâs worth mentioning that with the latest dialyxir (1.0.0-rc7) and Erlang/OTP 22.2, and the overspecs flag that Jose mentioned, dialyzer does catch the error as expected with the example:
lib/test_types.ex:15: The success typing for 'Elixir.TestTypes':hello/1 implies that the function might also return 'wtf' but the specification return is map()
Unfortunately, overspecs is noisy and fails, for example this function and spec:
@spec hi(integer()) :: :foo | :bar
def hi(n) do
if n < 5 do
:foo
else
:bar
end
end
with:
Type specification is a subtype of the success typing.
Function:
TestTypes.hi/1
Type specification:
@spec hi(integer()) :: :foo | :bar
Success typing:
@spec hi(_) :: :bar | :foo
You can fix it by adding def hi(n) when is_integer(n) do.
So it seems like overspecs might be something you could add when youâre starting a project, but hard to enable after the fact.
Thanks for the follow up but Iâm still not sure which is true. In the paragraph before that @rvirding says:
Also in some ways dialyzer basically ignores a function spec when type checking so you donât really need one to get the type checking. If you give one it will check that it is consistent with what it can work out about the function and it will use it when reporting type errors,
Iâm reading it as âdialyzer ignores elixir typespecsâ.
As I said it both ignores and doesnât ignore the erlang/elixir typespecs.
When it does its type checking it will look the code definition of a function and the calls to that function and check if they are consistent. For example if a function expects its first argument to be a list is it will check whether it is called with list. This it does irrespective of whether there is an explicit typespec or not. This means you donât need typespecs to get dialyzer to check your code.
If there is a typespec then it will check the typespec against the function definition to see whether they are consistent and report if they are not. It will also use the typespec when reporting errors.
Try reading the âLearn You Some Erlangâ chapter on dialyzer and typing, https://learnyousomeerlang.com/dialyzer which explains what is going quite well. While it is in Erlang it should be quite easy to understand. I donât know of something similar written for Elixir.
Per your reply, to me it means that typespecs do not help in analysis but rather in reporting (unless I donât understand what analysis means). I wish I wasnât so confused.
The analysis can be improved by inclusion of type hints (called specs) but it can be useful even without those.
Exactly. Dialyzer will ensure that the typespecs youâve written down match what it inferred (a.k.a. that theyâre not wrong). It helps you to find cases, where your codebase is consistent based on what dialyzer infers, but itâs not consistent with the developers intention.
defmodule A do
# This is fine
def calc(a, b), do: a + b
def run, do: add(1.0, 1.0)
end
# v.s.
defmodule B do
# This is not
@spec calc(integer, integer) :: integer
def calc(a, b), do: a + b
# The call 'Elixir.B':calc(1.0,float()) breaks the contract (integer(),integer()) -> integer()
def run, do: add(1.0, 1.0)
end
Edit: One couldâve added guard clauses with when is_integer(a) and is_integer(b) as well, but I guess for an example it should be fine.