类型变量是类型语言中经常使用但又非常容易被误解的部分。由于一些错误,dialyzer
在强制执行其定义的语义方面一直很宽松,这导致许多用户认为它们像大多数其他语言中那样是简单的泛型。由于大量的代码在编写时考虑到了这一点,OTP 无法修复导致 dialyzer
意外宽松的错误,而不会破坏大量代码的分析。
如果 dialyzer
是唯一可用的工具,那还好,但我们所知道的所有其他代码分析器都将类型变量解释为泛型,如果我们选择修复这些错误,就会导致生态系统碎片化。一个应用程序可能是在考虑 eqWAlizer
的情况下编写的,而另一个应用程序则是在考虑 dialyzer
的情况下编写的,从而阻止任何一个工具应用于整个版本。
由于 dialyzer
在这方面已经完全损坏,因此本 EEP 旨在通过将类型变量的定义从相等约束更改为泛型(参数多态)来解决此问题。我们还建议进行其他一些更改,以使类型语言更加有用且更少歧义。
正如当前文档所述,-spec
和 -type
中的变量的行为与普通代码中的行为相同。也就是说,如果变量 V
出现多次,则它在所有位置都具有相同的值。在像 C++ 或 Java 这样的语言中,所述值是一个类型,这工作得很好
template<typename V>
V foo(V a, V b) {
return a + b;
}
例如,foo(12, 34)
工作正常,因为两个参数都具有相同的类型 int
(为 T
生成 int
),但是 foo(12, 34.0)
不起作用,因为 int
不是 double
。
但是,由于 Erlang 类型语言的文档说明重复的变量引用的是运行时的相同值,因此 gradualizer
和 eqWAlizer
等工具不能自由地将它们视为泛型。例如
-spec my_add(T, T) -> T.
my_add(A, B) -> A + B.
在这里,唯一可行的 T
值是零的各种表示形式:foo(12, 34)
将失败,因为 12
不是 34
,而 foo(1, 1)
将失败,因为结果 2
不是 1
。
这使得无法声明一个函数采用某种类型的值并返回相同类型(但不一定是相同值)的东西。这在 dialyzer
中是有道理的,因为值和类型在其范例中是同一件事1,但限制了其他类型检查器可以执行的操作;尽管文档声明可以忽略类型变量提供的“额外信息”,但在考虑类型变量时,没有其他解释的余地。
`integer()` is. The type of `1` is simply `1`.
为了解决这个问题并使类型语言不那么令人惊讶,我们提出以下更改
将 ::
重新定义为别名,而不是“子类型”。
所有当前已知的工具都实现了此解释2。如果将来需要有界量化,则可以使用更广泛使用的 <:
或 =<
语法稍后引入“子类型”运算符。
拒绝在同一名称下使用多个别名,这将使以下内容非法
-spec multiple(X :: integer(), X :: integer()) -> atom().
-spec when_multiple(X) -> atom() when X :: integer(), X :: number().
为了防止由于此更改而破坏现有代码,编译器将不强制执行上述操作,而是将其留给类型检查工具(或可选的编译器标志)。
在解析器中规范化类型,替换别名并将“带注释的类型”视为与 when
相同。这使得以下签名等效
-spec xyzzy(A) -> term() when A :: number().
-spec waldo(B :: number()) -> term().
-spec fred(number()) -> term().
其中 B :: number()
仅仅是 B ... when B :: number()
的简写,并且所有变体都声明第一个参数是 number()
。
将类型变量视为泛型而不是相等约束。
示例
当前的 lists:append
类型规范是
-spec append(List1, List2) -> List3 when List1 :: [T], List2 :: [T], List3 :: [T], T :: term().
使用定义的语义,T :: term()
使 append
函数引用顶级类型。相反,以下是首选的,并且更符合当前的类型系统
-spec append(List1, List2) -> List3 when List1 :: [T], List2 :: [U], List3 :: [T | U].
让函数契约 (-spec
) 描述传入的参数类型。
函数契约一个非常令人惊讶的方面是,它们描述的是函数成功返回时的参数类型,并且不影响函数内部的参数类型。例如
-spec foo(integer()) -> integer().
foo(X) ->
bar(X), %% X is not necessarily an integer here!
X + 1.
大多数人认为上面的意思是传递给 bar/1
的类型是 integer()
,但它可以是传递给 foo/1
的任何类型,包括导致 foo/1
失败的值。这通常会隐藏掉被调用者(例如这里的 bar/1
)中存在无法访问的代码的事实,有时会导致用户认为永远不会发生的神秘警告。
这也使得用户更容易看到变量在任何点的类型:只需从 -spec
中声明的内容开始即可。目前,X
的类型完全取决于 X
是如何到达那里的,这并非总是很容易弄清楚。
一旦被接受,我们将提交一个包含文档更改的 PR 以反映上述内容
本文档置于公共领域或 CC0-1.0-Universal 许可之下,以更宽松者为准。