Dialyzer 无法使用多态类型识别函数中的错误
Dialyzer cannot recognize error in function using polymorphic types
背景
我正在尝试使用透析器进行多态输入。例如,我使用的是著名的 Option
类型(又名,Maybe 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
应该 return(根据规范定义)[{:some, String.t}] | []
.
这里的问题是,实际上,函数是 returning [{: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 只关心快乐的路径,那么这就可以解释问题(毕竟它被称为成功输入),但这也意味着它会完全错过我的代码中的一堆错误。
- 我对透析器的假设是否正确?
- 有没有办法让它更精确地发现错误,或者这是透析器使用的算法的限制? (因此无法修复)
also means it will totally miss a bunch of errors in my code.
您的理解是正确的,dialyzer 不是静态类型系统,它只能检测到导致确认类型冲突的错误子集。 detailed article 解释了透析器的设计和 trade-offs。
虽然有一些警告标志,例如 underspecs
/ overspecs
/ specdiffs
,但可以启用它们来检测更多类别的错误:可以找到列表 here and dialyxir supports them as command line options.
如果运行 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 将引入 two new flags:missing_return
和 extra_return
,分别类似于 overspecs
和 underspecs
,但更少误报,在实践中更有用。
missing_return
会捕获上面的 missing_range
示例,但不会像 overspecs
那样返回很多您可能并不真正关心的嘈杂 contract_subtype
警告。
总结
免责声明:这是我为寻找此问题的答案而冒险的总结。对于短版本,检查@sabiwara's .
在与许多人交谈后,我开始明白,只要我的代码中的 1 条路径导致成功执行并与我提供的规范兼容,dialyzer 就不会抱怨。
可能有很多路径中断,但只要 1 条有效,dialzyer 就会很高兴。
在我的具体情况下,因为我有一个 returns [{:some, String.t}]
透析器不抱怨的分支,因为我有 1 个成功的分支。
这可以在 Type Specifications and Eralng 的引用中得到更好的总结:
The other option is then to have a type system that will not prove the absence of errors, but will do a best effort at detecting whatever it can. You can make such detection really good, but it will never be perfect. It's a tradeoff to be made.
事实上,上面提到的文章,有一个例子和我自己的非常相似:
main() ->
X = case fetch() of
1 -> some_atom;
2 -> 3.14
end,
convert(X).
convert(X) when is_atom(X) -> {atom, X}.
这也不会触发透析器。根据文章:
From our point of view, it appears that at some point in time, the call to convert/1
will fail.
(...)
Dialyzer doesn't think so. (...) because there is the possibility that the function call to convert/1
succeeds at some point, Dialyzer will keep silent. No type error is reported in this case.
这很有启发性,我相信这就是我的情况。
透析器会明白发生了什么吗?
公平地说,如果我们使用一些标志,即 --overspecs
(对于这种情况)及其姊妹 --underspecs
(对于这种情况,我们不需要),透析器可以捕获此错误例)。
经过一番研究,我找到了一个邮件列表,其中以数学格式详细说明了这些标志的行为:
来自它:
Let SpecIn be a set of @spec inputs and RealIn be a set of inputs as inferred by Dialyzer from real code, then:
- The Input Contract is satisfied when SpecIn <= RealIn (where <= is a non-strict subset operation). See over_in in demo code below.
- The Input Contract violation is detected by -Wunderspecs option when SpecIn > RealIn. See under_in below.
It is easy to see in the code:
- It’s OK for over_in to declare that it only accepts :a and :b while it also happens to accept :c. Maybe suboptimal, but fine.
- It’s NOT OK for under_in to claim that it accepts :a, :b and :c and break if :c is passed. Rejecting :c would break the caller.
Let SpecOut be a set of @spec outputs and RealOut be a set of outputs as inferred by Dialyzer from real code, then:
- The Output Contract is satisfied when SpecOut >= RealOut (where >= is a non-strict superset operation). See under_out below.
- The Output Contract violation is detected by -Woverspecs option when SpecOut < RealOut. See over_out below.
It is easy to see in the code:
- It’s OK for under_out to declare that it returns :a, :b and :c, while currently it only returns :a and :b. Maybe future implementations will return :c as well.
- It’s NOT OK for over_out to declare that it returns :a and :b, but to also return :c sometimes. Returning :c would break the caller.
确实,如果我 运行 mix dialyzer --overspecs
使用此示例,dialzyer 确实会抱怨,因为:
The Output Contract violation is detected by -Woverspecs option when SpecOut < RealOut. See over_out below.
其中 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
这是一次穿越 Dialyzer 的疯狂旅程,老实说,我非常需要一次修订。在整个磨难过程中,我了解了几个透析器标志,并刷新了成功输入的记忆(我绝对需要这样做)。
感谢大家的参与!
背景
我正在尝试使用透析器进行多态输入。例如,我使用的是著名的 Option
类型(又名,Maybe 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
应该 return(根据规范定义)[{:some, String.t}] | []
.
这里的问题是,实际上,函数是 returning [{: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 只关心快乐的路径,那么这就可以解释问题(毕竟它被称为成功输入),但这也意味着它会完全错过我的代码中的一堆错误。
- 我对透析器的假设是否正确?
- 有没有办法让它更精确地发现错误,或者这是透析器使用的算法的限制? (因此无法修复)
also means it will totally miss a bunch of errors in my code.
您的理解是正确的,dialyzer 不是静态类型系统,它只能检测到导致确认类型冲突的错误子集。 detailed article 解释了透析器的设计和 trade-offs。
虽然有一些警告标志,例如 underspecs
/ overspecs
/ specdiffs
,但可以启用它们来检测更多类别的错误:可以找到列表 here and dialyxir supports them as command line options.
如果运行 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 将引入 two new flags:missing_return
和 extra_return
,分别类似于 overspecs
和 underspecs
,但更少误报,在实践中更有用。
missing_return
会捕获上面的 missing_range
示例,但不会像 overspecs
那样返回很多您可能并不真正关心的嘈杂 contract_subtype
警告。
总结
免责声明:这是我为寻找此问题的答案而冒险的总结。对于短版本,检查@sabiwara's
在与许多人交谈后,我开始明白,只要我的代码中的 1 条路径导致成功执行并与我提供的规范兼容,dialyzer 就不会抱怨。
可能有很多路径中断,但只要 1 条有效,dialzyer 就会很高兴。
在我的具体情况下,因为我有一个 returns [{:some, String.t}]
透析器不抱怨的分支,因为我有 1 个成功的分支。
这可以在 Type Specifications and Eralng 的引用中得到更好的总结:
The other option is then to have a type system that will not prove the absence of errors, but will do a best effort at detecting whatever it can. You can make such detection really good, but it will never be perfect. It's a tradeoff to be made.
事实上,上面提到的文章,有一个例子和我自己的非常相似:
main() ->
X = case fetch() of
1 -> some_atom;
2 -> 3.14
end,
convert(X).
convert(X) when is_atom(X) -> {atom, X}.
这也不会触发透析器。根据文章:
From our point of view, it appears that at some point in time, the call to
convert/1
will fail. (...) Dialyzer doesn't think so. (...) because there is the possibility that the function call toconvert/1
succeeds at some point, Dialyzer will keep silent. No type error is reported in this case.
这很有启发性,我相信这就是我的情况。
透析器会明白发生了什么吗?
公平地说,如果我们使用一些标志,即 --overspecs
(对于这种情况)及其姊妹 --underspecs
(对于这种情况,我们不需要),透析器可以捕获此错误例)。
经过一番研究,我找到了一个邮件列表,其中以数学格式详细说明了这些标志的行为:
来自它:
Let SpecIn be a set of @spec inputs and RealIn be a set of inputs as inferred by Dialyzer from real code, then:
- The Input Contract is satisfied when SpecIn <= RealIn (where <= is a non-strict subset operation). See over_in in demo code below.
- The Input Contract violation is detected by -Wunderspecs option when SpecIn > RealIn. See under_in below.
It is easy to see in the code:
- It’s OK for over_in to declare that it only accepts :a and :b while it also happens to accept :c. Maybe suboptimal, but fine.
- It’s NOT OK for under_in to claim that it accepts :a, :b and :c and break if :c is passed. Rejecting :c would break the caller.
Let SpecOut be a set of @spec outputs and RealOut be a set of outputs as inferred by Dialyzer from real code, then:
- The Output Contract is satisfied when SpecOut >= RealOut (where >= is a non-strict superset operation). See under_out below.
- The Output Contract violation is detected by -Woverspecs option when SpecOut < RealOut. See over_out below.
It is easy to see in the code:
- It’s OK for under_out to declare that it returns :a, :b and :c, while currently it only returns :a and :b. Maybe future implementations will return :c as well.
- It’s NOT OK for over_out to declare that it returns :a and :b, but to also return :c sometimes. Returning :c would break the caller.
确实,如果我 运行 mix dialyzer --overspecs
使用此示例,dialzyer 确实会抱怨,因为:
The Output Contract violation is detected by -Woverspecs option when SpecOut < RealOut. See over_out below.
其中 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
这是一次穿越 Dialyzer 的疯狂旅程,老实说,我非常需要一次修订。在整个磨难过程中,我了解了几个透析器标志,并刷新了成功输入的记忆(我绝对需要这样做)。
感谢大家的参与!