Erlang (Elixir) Dialyzer - 令人困惑的超类型错误

Erlang (Elixir) Dialyzer - confusing supertype error

我已经定义了一个 Elixir 行为 X。回调 start_link 指定为:

@callback start_link(
  args :: producer_args,
  opts :: GenServer.options
) :: GenServer.on_start

其中 producer_args 类型定义为:

@type producer_args :: %{job_queue_name: String.t}

在实现该行为的客户端代码Y中,start_link定义为:

def start_link(args = %{job_queue_name: _job_queue_name, redis_url: _redis_url}, opts) do
  GenStage.start_link(__MODULE__, args, opts)
end

透析器不喜欢它。它说,

(#{'job_queue_name':=_, 'redis_url':=_, _=>_}) 
is not a supertype of 
#{'job_queue_name':=binary()}

问题 #1:

在继承方面,子类型扩展超类型。因此,定义的行为(X)应该被认为是超类型。实现行为(Y)的模块应被视为子类型。显然 Dialyzer 应该问这个问题:

Is #{'job_queue_name':=binary()} a supertype of (#{'job_queue_name':=_, 'redis_url':=_, _=>_})?

而是反过来问问题。为什么?

问题 #2:

dialyzer中supertype的定义和OOP继承的讨论一样吗?如果不是,那是什么?我试图在透析器的上下文中找到超类型的定义,但发现 none.

错误消息基本上是说:您不能要求额外的 redis_url 键,因为它没有在行为的类型规范中声明。

Dialyzer 不将行为和实现模块视为类型。 它专门查看回调的参数。

将与 #{'job_queue_name':=_, 'redis_url':=_, _=>_} 匹配的值集是将与 #{'job_queue_name':=_} 匹配的值的子集。

所以 #{'job_queue_name':=_, 'redis_url':=_, _=>_}#{'job_queue_name':=_} 的子类型。

Dialyzer 将允许您使用回调中声明的超类型参数实现回调,因为这确保任何依赖于行为契约的代码都不会在运行时因匹配错误而失败。

关于你的第二个问题:

简短的回答是否定的。

长答案有点复杂。 Erlang/Elixir 本质上是动态语言(主要是因为 !receive 必须处理任何类型)并且很难将任何静态类型检查引入该语言。因此,以 Succes Types 和 Dialyzer 的形式提出并实施了新的解决方案。

你可以找到关于此事的工具介绍here and formal definition in original paper. And I could recommend recent talk

它不像其他语言那样直接,根据我的经验,很难找到这方面的专家。错误信息是隐秘的,即使是那些使用它相当长一段时间的人也经常猜测实际上知道他是怎么来的错误是什么。也就是说,这是一个非常有用的工具,他吐出的任何消息都值得研究。

一些值得注意的事情是:

  • 错误不是由函数定义中的不匹配规范生成的,而是由具有具体参数的函数调用生成的。那是你应该寻找错误的地方。
  • 添加规范并不总是有帮助。一个很好的例子是演讲中介绍的 add 函数。在大多数人会使用 (bool(), bool()) 的地方,正确的类型是 (any(), any()) 甚至 (false, any()) | (any(), false) | (true, true)。有时您需要做的就是修改规格。
  • 如果您可以使用 typer 找出 dialazer 导出的函数规范。