【问题标题】:Dialyzer does not catch errors on returned functionsDialyzer 不会捕获返回函数的错误
【发布时间】:2022-02-22 18:06:53
【问题描述】:

背景

在使用透析器、typespecs 和 currying 时,我能够在透析器中创建一个误报示例。

出于MWE 的目的,我使用diallyxir(包括版本),因为它让我的生活更轻松。 dialyxir 的作者确认这不是他们的问题,因此暂时排除了这种可能性。

环境

$ elixir -v
Erlang/OTP 24 [erts-12.2.1] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit]
Elixir 1.13.2 (compiled with Erlang/OTP 24)
  • 您使用的是哪个版本的 Dialyxir? (cat mix.lock | grep dialyxir):
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},

当前行为

给出以下代码示例:

defmodule PracticingCurrying do

  @spec greater_than(integer()) :: (integer() -> String.t())
  def greater_than(min) do
    fn number -> number > min end
  end

end

显然输入错误,我收到一条成功消息:

$ mix dialyzer
Compiling 1 file (.ex)
Generated grokking_fp app
Finding suitable PLTs
Checking PLT...
[:compiler, :currying, :elixir, :gradient, :gradualizer, :kernel, :logger, :stdlib, :syntax_tools]
Looking up modules in dialyxir_erlang-24.2.1_elixir-1.13.2_deps-dev.plt
Finding applications for dialyxir_erlang-24.2.1_elixir-1.13.2_deps-dev.plt
Finding modules for dialyxir_erlang-24.2.1_elixir-1.13.2_deps-dev.plt
Checking 518 modules in dialyxir_erlang-24.2.1_elixir-1.13.2_deps-dev.plt
Adding 44 modules to dialyxir_erlang-24.2.1_elixir-1.13.2_deps-dev.plt
done in 0m24.18s
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.ImmutableValues.beam',
   '/home/user/Workplace/fl4m3/grokking_fp/_build/dev/lib/grokking_fp/ebin/Elixir.PracticingCurrying.beam',
   '/home/user/Workplace/fl4m3/grokking_fp/_build/dev/lib/grokking_fp/ebin/Elixir.TipCalculator.beam'],
  warnings: [:unknown]
]
Total errors: 0, Skipped: 0, Unnecessary Skips: 0
done in 0m1.02s
done (passed successfully)

预期行为

我希望透析器会告诉我正确的规格是 @spec greater_than(integer()) :: (integer() -> bool())

作为旁注(和比较,如果你愿意的话)gradient 确实发现了错误。 我知道比较这些工具就像比较橘子和苹果,但我认为还是值得一提。

问题

  1. 透析器不打算捕获此类错误吗?
  2. 如果它应该捕获错误,可能会出现什么故障? (是我的例子不正确,还是透析器内部的东西?)

我个人很难相信这可能是 Dialyzer 中的错误,该工具已被很多人广泛使用,我是第一个发现此错误的人。但是,我无法解释发生了什么。

感谢您的帮助。

【问题讨论】:

  • Dialyzer 似乎在实际调用匿名函数之前不会分析它的返回类型。但在这种情况下,一旦你尝试调用你的函数,它仍然会抱怨:添加一个调用greater_than(1).(2)main 函数将给出Function main/0 has no local return.。不理想且非常神秘,但它仍然有助于捕获错误。这个伟大的article 可能有助于解释透析器的一些限制。
  • 这些是完全不同的例子,你原来的例子是关于匿名函数的,这个是有条件的。 overspecs/underspecs 标志可以帮助捕获更多类别的错误,但它们不会使透析器成为静态类型系统:有很多错误它永远无法捕获。
  • 我不是在谈论调用函数,而是在这种精确的情况下,当调用这个匿名函数时,不是只通过声明它来检查返回类型:)
  • 所以,如果我理解正确,对于匿名函数,Dialyzer 只会在调用它们时检查它们。这个对吗?如果是这样,请随时在正式的 SO 回答中解释我们的交流,以便我接受。
  • 完成。在尝试写答案时,我有一些有趣的发现,我希望它更清楚。这是一个有趣的搜索主题,谢谢分享。

标签: functional-programming elixir typing dialyzer


【解决方案1】:

Dialyzer 的分析相当乐观,忽略了某些类别的错误。 This article 提供了一些关于其方法和限制的高级解释。

在匿名函数的特殊情况下,dialyzer 似乎执行了非常少的检查 当它们被声明时:它将忽略其参数的类型和返回类型,例如 即使明显错误,以下内容也不会导致任何错误:

# no error
@spec add(integer()) :: (String.t() -> String.t())
def add(x) do
  fn y -> x + y end
end

然而,它会指出数量不匹配,例如

# invalid_contract
# The @spec for the function does not match the success typing of the function.

@spec add2(integer()) :: (integer(), integer() -> integer())
def add2(x) do
  fn y -> x + y end
end

在尝试使用匿名函数时,Dialyzer 可能能够检测到类型冲突, 但这不能保证(请参阅上面的文章),并且错误消息可能没有帮助:

# Function main/0 has no local return.

def main do
  positive? = greater_than(0)
  positive?.(2)
end

我们不知道究竟是什么问题,甚至不知道导致错误的行。但至少我们知道有一个并且可以调试它。

在以下示例中,错误信息更多(使用:lists.map/2 而不是Enum.map/2,因为 dialyzer 不理解可枚举协议):

# Function main2/0 has no local return.

def main2 do
  positive? = greater_than(0)

  # The function call will not succeed.
  # :lists.map(_positive? :: (integer() -> none()), [-2 | 0 | 1, ...])
  # will never return since the success typing arguments are
  # ((_ -> any()), [any()])

  :lists.map(positive?, [1, 0, -2])
end

这告诉我们,dialyzer 推断出greater_than/1 的返回类型为(integer() -> none())none在上面的文章中描述为:

这是一种特殊类型,表示没有任何术语或类型是有效的。 通常,当 Dialyzer 将函数的可能返回值归结为none() 时,这意味着该函数应该崩溃。 它是“这东西行不通”的同义词。

所以dialyzer知道这个函数不能被成功调用,但是在实际调用之前不会认为它是类型冲突,所以它会允许声明(以同样的方式你可以完美地创建一个函数,只是@987654332 @s)。

免责声明:我找不到关于透析器如何处理匿名的官方解释 功能详细,所以以上解释是基于我的观察和解释

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-08-09
    • 1970-01-01
    • 1970-01-01
    • 2022-09-24
    • 1970-01-01
    • 1970-01-01
    • 2021-12-01
    相关资源
    最近更新 更多