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}] | nil
。nil
与空列表[]
不同。
问题
鉴于这个问题,我希望透析器会抱怨。然而,它很乐意接受这个错误的规范:
$ 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_return
和extra_return
,分别类似于overspecs
和underspecs
,但误报更少,在实践中更有用。
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
这是一次穿越透析器的疯狂旅程,也是我非常诚实地需要的修订。在整个磨难中,我了解了几个透析器标志,并在成功打字时刷新了我的记忆(我绝对需要)。
感谢大家的参与!