Dialyzer has several limitations as a means of type checking. Dialyzer is not a strict type, it is a free type. This means that he will only give you a warning when he finds something clearly wrong with the way the function is declared, and not when he indicates that the caller can do something bad.
He will try to make a conclusion about site calls, but he cannot go beyond what the main declarations ofpec types can convey. Thus, an integer value can be defined as neg_integer() , a pos_integer() , a non_neg_integer() or any integer() , but if you do not have clearly defined boundaries for the legal value, there is no way to determine an arbitrary range from, say, 5..infinity , but you can define a range, for example 5..10 , and get the expected result.
The odd part of this is that although the guards provide some Dialyzer information because it is a permissive / free font, the real load on the encoder is on specification functions with fairly narrow definitions that errors on calling sites can be detected.
This is how it happens in the actual release of + Dialyzer code (carry me, its a little long screen to show it all in full, but nothing demonstrates the corresponding problem better than the code):
Original problem
-module(dial_bug1). -export([test/0]). %-export([f/1]). test() -> f(1). f(X) when X > 5 -> X * 2.
Dealer Days:
dial_bug1.erl:5: Function test/0 has no local return dial_bug1.erl:8: Function f/1 has no local return dial_bug1.erl:8: Guard test X::1 > 5 can never succeed done in 0m1.42s done (warnings were emitted)
So, in a closed world, we see that Dialyzer will return to the caller, because he has a limited case.
Second option
-module(dial_bug2). -export([test/0]). -export([f/1]). test() -> f(1). f(X) when X > 5 -> X * 2.
The dealer says:
done (passed successfully)
In an open world where someone sending something can be the caller, there is no effort to step back and check the undeclared unlimited range.
Third option
-module(dial_bug3). -export([test/0]). -export([f/1]). -spec test() -> integer(). test() -> f(-1). -spec f(X) -> Result when X :: pos_integer(), Result :: pos_integer(). f(X) when X > 5 -> X * 2.
The dealer says:
dial_bug3.erl:7: Function test/0 has no local return dial_bug3.erl:8: The call dial_bug3:f(-1) breaks the contract (X) -> Result when X :: pos_integer(), Result :: pos_integer() done in 0m1.28s done (warnings were emitted)
In the open world, where we have a declared open range (in this case, a set of positive integers), an abusive calling site will be found.
Fourth option
-module(dial_bug4). -export([test/0]). -export([f/1]). -spec test() -> integer(). test() -> f(1). -spec f(X) -> Result when X :: pos_integer(), Result :: pos_integer(). f(X) when 5 =< X, X =< 10 -> X * 2.
The dealer says:
done (passed successfully)
In an open world where we have a guarded but still unannounced range, we find that Dialyzer will not find the caller again. This is the most important option of everything, in my opinion, because we know that Dialyzer really takes hints from the guards checking the types, but apparently it does not take hints from the numerical checks. So let's see if we declare a limited but arbitrary range ...
Fifth option
-module(dial_bug5). -export([test/0]). -export([f/1]). -spec test() -> integer(). test() -> f(1). -spec f(X) -> Result when X :: 5..10, Result :: pos_integer(). f(X) when 5 =< X, X =< 10 -> X * 2.
The dealer says:
dial_bug5.erl:7: Function test/0 has no local return dial_bug5.erl:8: The call dial_bug5:f(1) breaks the contract (X) -> Result when X :: 5..10, Result :: pos_integer() done in 0m1.42s done (warnings were emitted)
And here we see that if we place the Dialyzer, it will do its job as expected.
I’m not sure if this is considered a “mistake” or a “limitation of the Dialysis rift”. The main cause of pain for Dialyzer addresses is unsuccessful native types, not numerical boundaries.
All that said ...
When I ever had this problem in actual working code in a real project, useful in the real world, I already know in advance whether I am dealing with reliable data or not, and in very few cases I wouldn’t always write this :
- Crash outright (Never return a bad result! Just die instead! This is our religion for some reason.)
- Return the wrapped value of the form
{ok, Value} | {error, out_of_bounds} {ok, Value} | {error, out_of_bounds} and let the caller decide what to do with him (this gives them the best information in each case).
The confirmed example is relevant - the last example above, which has limited protection, would be a suitable version of the reset function.
-spec f(X) -> Result when X :: 5..10, Result :: {ok, pos_integer()} | {error, out_of_bounds}. f(X) 5 =< X, X =< 10 -> Value = X * 2, {ok, Value}; f(_) -> {error, out_of_bounds}.