I am trying out polymorphic typing with dialyzer. As an example I am using the famous Option
type (aka, Maybe Monad) that is now prevalent in many other languages these days.
defmodule Test do
@type option(t) :: some(t) | nothing
@type some(t) :: [{:some, t}]
@type nothing :: []
@spec validate_name(String.t()) :: option(String.t())
def validate_name(name) do
if String.length(name) > 0 do
[{:some, name}]
else
nil
end
end
end
As you can see, the function validate_name
should return (by spec definition) [{:some, String.t}] | []
.
The issue here is that in reality, the function is returning [{:some, String.t}] | nil
. nil
is not the same as empty list []
.
Given this issue, I would expect dialyzer to complain. However it gladly accepts this erroneous spec:
$ mix dialyzer
Compiling 1 file (.ex)
Finding suitable PLTs
Checking PLT...
[:compiler, :currying, :elixir, :gradient, :gradualizer, :kernel, :logger, :stdlib, :syntax_tools]
PLT is up to date!
No :ignore_warnings opt specified in mix.exs and default does not exist.
Starting Dialyzer
[
check_plt: false,
init_plt: '/home/user/Workplace/fl4m3/grokking_fp/_build/dev/dialyxir_erlang-24.2.1_elixir-1.13.2_deps-dev.plt',
files: ['/home/user/Workplace/fl4m3/grokking_fp/_build/dev/lib/grokking_fp/ebin/Elixir.Book.beam',
'/home/user/Workplace/fl4m3/grokking_fp/_build/dev/lib/grokking_fp/ebin/Elixir.DealingWithListsOfLists.beam',
'/home/user/Workplace/fl4m3/grokking_fp/_build/dev/lib/grokking_fp/ebin/Elixir.Event.beam',
'/home/user/Workplace/fl4m3/grokking_fp/_build/dev/lib/grokking_fp/ebin/Elixir.FlatMapsVSForComprehensions.beam',
'/home/user/Workplace/fl4m3/grokking_fp/_build/dev/lib/grokking_fp/ebin/Elixir.ImmutableValues.beam',
...],
warnings: [:unknown]
]
Total errors: 0, Skipped: 0, Unnecessary Skips: 0
done in 0m1.09s
done (passed successfully)
Furthermore, no matter what I put in the else
branch, the result is always a "happy dialyzer".
At this point, the only logical solution I can think of is that dialyzer is only concerned with the happy path. Meaning, it will ignore my else
branch.
If dialzyer is only ever concerned with happy paths, then this would explain the issue (it is called success typing after all) but it also means it will totally miss a bunch of errors in my code.
also means it will totally miss a bunch of errors in my code.
Your understanding is correct, dialyzer is not a statical type system and it will only be able to detect a subset of errors that will result in a confirmed type clash. This detailed article explains the design and trade-offs behind dialyzer.
There are some warning flags though, like underspecs
/ overspecs
/ specdiffs
, than can be enabled to detect more categories of errors: the list can be found here and dialyxir supports them as command line options.
If running mix dialyzer --overspecs
(or --specdiffs
), you should get:
your_file.ex:6:missing_range
The type specification is missing types returned by function.
Function:
Test.validate_name/1
Type specification return types:
[{:some, binary()}]
Missing from spec:
nil
Running mix dialyzer.explain missing_range
:
Function spec declares a list of types, but function returns value
outside stated range.
This error only appears with the :overspecs flag.
Edit: Since OTP 25, Dialyzer will introduce two new flags: missing_return
and extra_return
, similar respectively to overspecs
and underspecs
, but with less false positives and more usable in practice.
missing_return
will catch the missing_range
example above, but without returning a lot of noisy contract_subtype
warnings that you might not really care about, like overspecs
does.