问题描述
Dialyzer 2.9版. Erts 7.3.一次性密码18.
在以下人为设计的erlang代码中:
-module(dialBug).
-export([test/0]).
%-export([f1/1]). % uncomment this line
test() ->
f1(1).
f1(X) when X > 5 ->
X*2.
在上述代码上运行透析器时,它会警告该代码将无法工作,因为防护测试(X> 5)永远不会成功.
但是,当我取消注释第三行并导出f1/1函数透析器时,不再发出任何警告.
我认识到,导出f1/1时,透析器不可能知道Guard子句将失败,因为外部客户端可以使用它.但是,为什么它不能再确定test/0是否错误地使用了f1/1?
Dialyzer作为类型检查器有一些限制. Dialyzer不是 strict 打字机,而是 loose 打字机.这意味着仅在发现函数声明方式明显有误的情况下才会发出警告,而不是在某种情况下会推断出调用方可能做错了事情.
它将尝试推断有关调用站点的信息,但不能超出基本的typespec声明可以传达的内容.因此,可以将整数值定义为neg_integer()
,pos_integer()
,non_neg_integer()
或任何integer()
,但是除非您明确定义了合法值的边界,否则无法从例如5..infinity
,但是您可以定义一个范围,例如5..10
并获得您期望的结果.
奇怪的是,尽管警卫人员向Dialyzer提供了一些信息,但由于这是一种允许的/松散的键入程序,编码器必须对编码器进行足够严格的定义,以确保可以在调用站点处检测到错误./p>
以下是这些在实际代码+ Dialyzer输出中的显示方式(请耐心等待,它有点长,可以完整显示所有内容,但是没有比代码更好地说明相关问题了)
原始问题
-module(dial_bug1).
-export([test/0]).
%-export([f/1]).
test() ->
f(1).
f(X) when X > 5 ->
X * 2.
透析器天数:
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)
因此,在封闭的世界中,我们可以看到Dialyzer的情况有限,它将回退到呼叫者.
第二个变种
-module(dial_bug2).
-export([test/0]).
-export([f/1]).
test() ->
f(1).
f(X) when X > 5 ->
X * 2.
透析器说:
done (passed successfully)
在开放的世界中,呼叫者可以是发送任何东西的任何人,因此无需努力回溯并检查未声明的,无界的范围.
第三种变体
-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.
透析器说:
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)
在一个开放的世界中,我们有一个可声明的开放范围(在这种情况下,是正整数集),将找到有问题的呼叫站点.
第四种变体
-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.
透析器说:
done (passed successfully)
在一个开放的世界中,我们有一个受保护的但仍未声明的范围,我们发现Dialyzer将再次找不到令人讨厌的呼叫者.在我看来,这是所有方法中最重要的一种-因为我们知道Dialyzer确实接受了检查类型的后卫的提示,但很显然,它却没有接受检查类型的提示.数字范围检查防护.因此,让我们看看是否声明一个有界但任意的范围...
第五个变种
-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.
透析器说:
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)
在这里我们看到,如果我们用勺子喂Dialyzer,它将按预期完成工作.
我真的不确定这是否被视为漏洞"或透析器松动的约束". Dialyzer解决的主要问题是失败的本机类型,而不是数字范围.
所有所说的...
当我在现实世界中有用的实际项目中遇到过实际问题时,我已经很清楚地知道我是否在处理有效数据,在极少数情况下,我不知道该如何处理.我不会总是这样写的:
- 完全崩溃(从不返回错误的结果!只是死去!这是我们的宗教信仰.)
- 返回形式为
{ok, Value} | {error, out_of_bounds}
的包装值,并让调用者决定如何处理它(这在每种情况下都为他们提供了更好的信息).
一个受保护的示例是相关的-上面最后一个具有有限保护的示例将是可崩溃函数的正确版本.
-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}.
Dialyzer version 2.9. Erts 7.3. OTP 18.
In the following contrived erlang code:
-module(dialBug).
-export([test/0]).
%-export([f1/1]). % uncomment this line
test() ->
f1(1).
f1(X) when X > 5 ->
X*2.
When dialyzer is run over the above code it warns that the code won't work as the guard test (X > 5) can never succeed.
However, when I uncomment the 3rd line and export the f1/1 function dialyzer no longer issues any warnings.
I realise that when f1/1 is exported it is impossible for dialyzer to know that the guard clause will fail as external clients can use it. However, why can it no longer determine that test/0 is using f1/1 incorrectly?
Dialyzer have a few limitations as a type checker. Dialyzer is not a strict typer, it is a loose typer. What this means is that it will only give you a warning when it finds something that is clearly wrong with the way a function is declared as opposed to a certain case where it infers that a caller may be doing something bad.
It will try to infer things about calling sites, but it cannot go beyond what basic typespec declarations can convey. So an integer value can be defined as a neg_integer()
, a pos_integer()
, a non_neg_integer()
, or any integer()
, but unless you have clearly defined boundaries to the legal value there is no way to define an arbitrary range from, say, 5..infinity
, but you can define a range such as 5..10
and get the result you expect.
The odd part of this is that while guards provide some information to Dialyzer, because it is a permissive/loose typer the real burden is on the coder to spec functions with tight enough definitions that errors at calling sites can be detected.
Here is how these things play out in actual code + Dialyzer output (bear with me, its a bit of a long screen to show all of this completely, but nothing demonstrates the relevant issue better than code):
Original problem
-module(dial_bug1).
-export([test/0]).
%-export([f/1]).
test() ->
f(1).
f(X) when X > 5 ->
X * 2.
Dialyzer 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 can see Dialyzer will backtrack to the caller because it has a limited case.
Second variant
-module(dial_bug2).
-export([test/0]).
-export([f/1]).
test() ->
f(1).
f(X) when X > 5 ->
X * 2.
Dialyzer says:
done (passed successfully)
In an open world, where the caller could be anybody sending anything, there is no effort to backtrack and check an undeclared, unbounded range.
Third variant
-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.
Dialyzer 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 an open world where we have a declarable open range (in this case, the set of positive integers) the offending calling site will be found.
Fourth variant
-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.
Dialyzer says:
done (passed successfully)
In an open world where we have a guarded but still undeclared range we find that Dialyzer will once again not find the offending caller. This is the most significant variant of all, in my opinion -- because we know that Dialyzer does take hints from guards that check types, but evidently it does not take hints from numeric range checking guards. So let's see if we declare a bounded, but arbitrary, range...
Fifth variant
-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.
Dialyzer 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 spoon-feed Dialyzer it will do its job as expected.
I am not really sure whether this is considered a "bug" or "a constraint of Dialyzer's looseness". The main point of pain Dialyzer addresses are failed native types, and not numeric bounds.
All that said...
When I have ever had this problem in actual, working code in an actual project useful in the real world -- I already know well in advance whether I am dealing with valid data or not, and in the very few cases I don't I would always write this to either:
- Crash outright (Never return a bad result! Just die instead! This is our religion for a reason.)
- Return a wrapped value of the form
{ok, Value} | {error, out_of_bounds}
and let the caller decide what to do with it (this gives them better information in every case).
A guarded example is relevant -- the final example above that has a bounded guard would be the proper version of a crashable 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}.
这篇关于导出功能时,透析器未检测到违反保护措施的情况的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!