6

透析器 2.9 版。厄特 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) 永远不会成功。

但是,当我取消注释第 3 行并导出 f1/1 函数时,透析器不再发出任何警告。

我意识到当 f1/1 被导出时,透析器不可能知道保护子句将失败,因为外部客户端可以使用它。但是,为什么它不能再确定 test/0 错误地使用了 f1/1 呢?

4

1 回答 1

7

Dialyzer 作为类型检查器有一些限制。Dialyzer 不是严格的 typer,它是一个松散的 typer。这意味着它只会在发现函数的声明方式明显错误时才会向您发出警告,而不是在某些情况下推断调用者可能在做坏事。

它将尝试推断有关调用站点的内容,但它不能超出基本类型规范声明可以传达的内容。因此,整数值可以定义为 a neg_integer()、 a pos_integer()、 anon_neg_integer()或 any integer(),但除非您明确定义了合法值的边界,否则无法定义任意范围,例如5..infinity,但您可以定义一个范围,例如5..10并获得您期望的结果。

奇怪的是,虽然守卫向 Dialyzer 提供了一些信息,但因为它是一个宽松/松散的类型器,真正的负担在于编码器来规范具有足够严格定义的函数,以便可以检测到调用站点的错误。

以下是这些事情在实际代码 + 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 松散的约束”。主要的痛点是透析器地址是失败的本地类型,而不是数字界限

说了这么多...

当我在实际项目中遇到这个问题时,在现实世界中有用的实际项目中的工作代码 - 我已经提前知道我是否正在处理有效数据,并且在极少数情况下我不知道总是把它写到:

  1. 彻底崩溃(永远不要返回糟糕的结果!只是死了!这是我们的宗教信仰。)
  2. 返回表单的包装值{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}.
于 2017-09-17T11:35:13.183 回答