此 EEP 建議新增一個內建類型 dynamic()
(除了現有的內建類型 term()
之外),使 Erlang 對於漸進式類型檢查更加友善。針對漸進式類型使用特殊的動態類型是一種廣泛採用的產業解決方案。
Erlang 是許多程式語言之一,它們最初是純動態類型的語言,沒有靜態類型資訊,但後來加入了擴充功能(或方言)以在原始碼中指定類型資訊(類型提示、規格)。
Erlang 透過 EEP 8(類型和函式規格)擴充了規格。
目前關於類型和規格的文件可以在 https://erlang.dev.org.tw/doc/reference_manual/typespec.html 找到。
除了用於文件目的外,工具也可以使用類型和規格進行靜態分析。現有的工具包括
Dialyzer 在成功類型化的範式中使用類型規格(在 Pull Request 6281「記錄規格的意義」和最初的 論文「一種在 Erlang 中指定類型契約的語言及其與成功類型化的互動」中記錄)。值得注意的是,dialyzer 處理規格的方式與傳統的類型檢查器非常不同。
eqWAlizer 以傳統方式使用類型規格來執行類型檢查。
截至目前,採用或改裝靜態類型到動態類型語言的主要產業方法是透過漸進式類型化(https://en.wikipedia.org/wiki/Gradual_typing、http://samth.github.io/gradual-typing-bib)。漸進式類型化具有特殊的類型 dynamic
或 ?
,它允許以使用者友善的方式混合未類型化(或動態類型化)和靜態類型化的程式碼,並且也有助於在大型專案中逐步採用靜態類型化。針對漸進式類型使用特殊的動態類型是一種廣泛採用的解決方案。
由於 Erlang 在表面語法中沒有內建的動態類型,eqWAlizer 必須引入自己的類型 eqwalizer:dynamic()
,並為漸進式類型化設定特殊的語意。
term()
/any()
和透過範例進行子類型化 #在開始討論動態類型之前,值得釐清內建 Erlang 類型 term()
和 any()
的意義和語意。
any()
是子類型格的頂層類型元素。這在 Erlang 文件 中明確指出term()
是 any()
的別名。它們是相同的類型。在本文檔的其餘部分,我們將使用 term()
,因為它更具 Erlang 特性。term()
類型與其他語言和類型檢查器中的以下類型類似
object
在 Python 中(另請參閱 typing 文件 中關於 object 的詳細資訊)。mypy、PyRight、Pyre 以相同方式處理 object
。unknown
在 Typescript 中top
(https://github.com/ruby/rbs/blob/master/docs/syntax.md)unknown
mixed
mixed
從傳統靜態類型的觀點來看,在預期使用更具體的類型的地方使用 term()
值是一種類型錯誤。
讓我們看看範例。
範例 1. Erlang,eqWAlizer
-module(example).
-spec foo(term()) -> number().
foo(N) ->
N.
^ % type error. expected: number(), got: term()
-spec n(number()) -> number().
n(N) -> N.
-spec foo_n(term()) -> number().
foo_n(N) ->
n(N).
^ type error. expected: number(), got term()
範例 2. Python,mypy
def foo(n: object) -> int:
return n
~ Incompatible return value type (got "object", expected "int")
def n_fun(n: int) -> int:
return n
def foo_n(n: object) -> int:
return n_fun(n)
~ Argument 1 to "n_fun" has incompatible type "object";
expected "int" [arg-type]
範例 3. TypeScript
function foo(x: unknown): number {
return x;
~~~~~~~~~ Type 'unknown' is not assignable to type 'number'.(2322)
}
function n_fun(n: number): number {
return n;
}
function foo_n(n: unknown): number {
return n_fun(n);
~ Argument of type 'unknown' is not assignable
to parameter of type 'number'
}
dynamic()
類型 #eqWAlizer 有一個特殊的類型 eqwalizer:dynamic()
,其文件位於 此處。
此類型與其他語言和類型檢查器中的以下類型類似
eqWAlizer 從 Hack 借用了這個名稱。命名選擇是由以下考量因素所決定的
eqwalizer:any()
會因為內建的 Erlang 類型 any()
而產生誤導和混淆eqwalizer:dynamic()
通常用於標記「固有動態程式碼」:從 ETS 讀取、訊息傳遞、反序列化等等。(在本文檔的其餘部分,我們僅使用 dynamic()
作為 eqwalizer:dynamic()
的縮寫。)
引入 dynamic()
類型的基本原理在 eqWAlizer 文件 和其他語言和類型檢查器中已經提到的資源中給出。
我們建議使用新的 dynamic()
內建類型來擴充 Erlang 表面語法,使 Erlang 對於建立在漸進式類型化(例如 eqWAlizer 或 Gradualizer)概念之上的工具更加友善,並簡化其採用(包括將其用於鍵入 OTP 函式庫)。
有人可能會認為,只有一個類型 - term()
/any()
可能就足夠了,並且由工具將其解釋為適當的頂層類型或作為漸進式類型化的特殊動態類型。然而,從長遠來看,這將會造成限制。
其他類似語言和類型檢查器的故事都呈現出一個共同點:它們最終都選擇為不同的嚴格性和保證層級提供選項或模式。或者,粗略地說 - 它們至少有兩種模式:漸進模式和嚴格模式。漸進模式針對非侵入性的增量採用進行最佳化,而嚴格模式針對類型安全和強大的健全性保證進行最佳化。
term()
和 dynamic()
而不引入歧義或混淆變得非常重要。dynamic()
類型可以作為一種逃生機制。term()
類型,以確保沒有任何內容逃脫類型檢查。term()
/any()
也會產生問題。最初,Gradualizer 將 any()
用於動態類型,並將 term()
用作頂層類型。根據 Gradualizer 作者的說法,這證明會造成混淆,並且與這些類型的先前使用不相容。在內部,dialyzer 已經將 any()
/term()
作為規格方面的 dynamic()
來使用。
一個範例
-module(example).
-export([get_foo/1, get_bar/1, get_moo/1]).
-record(rec, {
foo :: term() | atom(),
bar :: number() | atom(),
moo :: term()
}).
-spec get_foo(#rec{}) -> number().
get_foo(R) -> R#rec.foo.
-spec get_bar(#rec{}) -> number().
get_bar(R) -> R#rec.bar.
-spec get_moo(#rec{}) -> number().
get_moo(R) -> R#rec.moo.
執行 dialyzer(dialyzer example.erl
)會產生以下輸出
example.erl:14:2: The success typing for example:get_bar/1 implies
that the function might also return atom()
but the specification return is number()
至於 eqwalizer:dynamic()
- 它定義為 any()
/term()
的別名,並且可以與 dialyzer 一起使用。(然而,以這種方式定義它會為迂腐和好奇的使用者帶來困惑。)
https://github.com/erlang/otp/pull/6993
使用 PR 6335(允許重新定義內建類型),此變更將在 OTP 26 中向後相容。
本文件置於公有領域或 CC0-1.0-通用授權之下,以更寬鬆者為準。