透析器无法使用多态类型识别功能错误



Background

我正在尝试使用透析器进行多态性打字。作为一个例子,我正在使用著名的Option类型(又名,也许是Monad),现在在许多其他语言中很普遍。

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

如您所见,函数validate_name应返回(根据规范定义)[{:some, String.t}] | []

这里的问题是,实际上,该函数正在返回[{:some, String.t}] | nilnil与空列表[]不同。

问题

鉴于这个问题,我希望透析器会抱怨。然而,它很乐意接受这个错误的规范:

$ 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)

此外,无论我在else分支中放入什么,结果总是"快乐的透析器"。

问题

在这一点上,我能想到的唯一合乎逻辑的解决方案是,透析器关心快乐的道路。意思是,它将忽略我的else分支。

如果dialzyer只关心快乐的路径,那么这将解释这个问题(毕竟它被称为成功打字),但这也意味着它将完全错过我的代码中的一堆错误。

  • 我对透析器的假设是否正确?
  • 有没有办法使其更精确地查找错误,或者这是透析器使用的算法的限制?(因此无法修复)

也意味着它将完全错过我的代码中的一堆错误。

您的理解是正确的,透析器不是静态类型系统,它只能检测到导致确认类型冲突的错误子集。这篇详细的文章解释了透析器背后的设计和权衡。

不过,有一些警告标志,例如underspecs/overspecs/specdiffs,可以启用以检测更多类别的错误:列表可以在这里找到,dialyxir支持它们作为命令行选项。

如果运行mix dialyzer --overspecs(或--specdiffs),您应该得到:

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

运行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.

编辑:自OTP 25以来,Dialyzer将引入两个新标志:missing_returnextra_return,分别类似于overspecsunderspecs,但误报更少,在实践中更有用。

missing_return将捕获上面的missing_range示例,但不会像overspecs那样返回许多您可能并不真正关心的嘈杂contract_subtype警告。

总结

免责声明:这是我寻找这个问题的答案的冒险总结。对于简短版本,请检查@sabiwara的响应。

在与许多人交谈后,我开始明白,只要我的代码中有 1 条路径导致与我提供的规范兼容的成功执行,透析器就不会抱怨。

可能有很多路径会中断,但只要 1 条有效,dialzyer 就会很高兴。

在我的特定情况下,因为我有一个分支返回[{:some, String.t}]透析器不会抱怨,因为我有 1 个分支成功。

这可以在类型规范和Eralng的引用中更好地总结:

另一种选择是拥有一个类型系统,该系统不会证明没有错误,但会尽最大努力检测任何可能的东西。你可以让这样的检测变得非常好,但它永远不会是完美的。这是一个需要做出的权衡。

其实上面提到的文章,有一个和我自己非常相似的案例:

main() ->
X = case fetch() of
1 -> some_atom;
2 -> 3.14
end,
convert(X).
convert(X) when is_atom(X) -> {atom, X}.

这也不会触发透析器。根据文章:

从我们的角度来看,似乎在某个时间点,对convert/1的调用将失败。 (...) 透析器不这么认为。(...)因为对convert/1的函数调用有可能在某个时候成功,所以 Dialyzer 将保持沉默。在这种情况下,不会报告任何类型错误。

这是非常有启发性的,我相信这就是我的情况。

透析器会明白发生了什么吗?

公平地说,如果我们使用一些标志,命名--overspecs(对于这种情况)及其姊妹--underspecs(在这种情况下我们不需要),透析器可能会捕获此错误。

经过一些研究,我能够找到一个邮件列表,以数学格式详细说明这些标志的行为:

  • [erlang-questions] 透析器规格不足和超规格选项

从它:

设 SpecIn 是一组@spec输入,RealIn 是 Dialyzer 从真实代码推断的一组输入,然后:

  • 当 SpecIn <= RealIn 时满足输入协定(其中 <= 是非严格子集操作)。请参阅下面的演示代码中的over_in。
  • 当 SpecIn> RealIn 时,-Wunderspecs 选项会检测到输入合约冲突。请参阅下面的under_in。

在代码中很容易看到:

  • over_in可以声明它只接受 :a 和 :b,同时它也恰好接受 :c。也许次优,但很好。
  • under_in声称它接受 :a、:b 和 :c 并在传递 :c 时中断是不行的。拒绝 :c 会破坏调用方。

设 SpecOut 是一组 @spec 输出,RealOut 是一组输出,由 Dialyzer 从真实代码推断出来,然后:

  • 当 SpecOut>= RealOut(其中>= 是非严格超集操作)时,输出协定得到满足。请参阅下面的under_out。
  • 当 SpecOut

在代码中很容易看到:

  • under_out可以声明它返回 :a、:b 和 :c,而目前它只返回 :a 和 :b。也许未来的实现也会返回:c。
  • over_out声明它返回 :a 和 :b 是不行的,但有时也返回 :c。返回 :c 会破坏调用方。

事实上,如果我使用此示例运行mix dialyzer --overspecs,dialzyer 确实会抱怨,因为:

输出合同冲突由 -Woverspecs 选项检测到 SpecOut <RealOut。请参阅下面的over_out。>

RealOut[] | [t] | nil,SpecOut[] | [t]。 因此,检测到合同违规。

显示错误:

lib/test.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

这是一次穿越透析器的疯狂旅程,也是我非常诚实地需要的修订。在整个磨难中,我了解了几个透析器标志,并在成功打字时刷新了我的记忆(我绝对需要)。

感谢大家的参与!

最新更新