此 EEP 提議在 Erlang 中新增名義型別 -nominal
,這些變更可以完全應用於 Dialyzer(模組化剖析)。作為副作用,名義型別可以編碼不透明型別,從而提高 Dialyzer 的可維護性。名義型別先前已在動態語言中使用,例如 Flow,或在 TypeScript 和靜態語言(如 Rust ( tuple 是結構型別,struct 是名義型別 )、OCaml ( record 是名義型別,object 是結構型別 )、Scala 和/或 Swift)中進行編碼。
Erlang 是一種動態型別語言,具有許多用於靜態型別檢查的可選工具。現有工具採用許多不同的型別範例,包括成功型別、漸進式型別和集合理論型別系統等。雖然所有這些型別系統在處理型別檢查和推斷的方式上有所不同,但它們都是結構型別系統。如果兩個型別的結構相同,則它們被視為等效。型別比較基於型別的結構,而不是使用者明確定義它們的方式。例如,在以下範例中,meter()
和 foot()
在結構型別系統中是等效的,因為它們具有相同的結構。這兩個型別可以互換使用。它們與基本型別 integer()
並無不同。
-type meter() :: integer().
-type foot() :: integer().
名義型別是一種替代型別系統,其中兩個型別等效的條件是它們以相同的型別名稱宣告。如果以上範例是在名義型別系統中,meter()
和 foot()
將不再相容,因為它們具有不同的名稱。每當函式預期 meter()
型別時,傳入 foot()
型別將會導致錯誤。名義型別可以防止意外誤用具有相同結構的型別。這是一個對 Erlang 所有現有型別系統都有用的正交特性。
名義型別可以被視為具有寬鬆語義的不透明型別。名義型別和不透明型別的共同點是名稱不同的型別不相容。名義型別允許對內部結構進行模式比對,這是使用不透明型別禁止的。除此之外,透過根據名義型別編碼不透明型別,我們可以使 Dialyzer 更快且更易於維護。
此 EEP 提議一個新的語法 -nominal
來宣告名義型別。它可以像以下範例一樣使用
-module(example).
% Declaration of nominal types
-nominal meter() :: integer().
-nominal foot() :: integer().
% Constructor for nominal types
-spec meter_ctor(integer()) -> meter().
meter_ctor(X) -> X.
% Function that has its input and/or output as nominal types
-spec meter_to_foot(meter()) -> foot().
meter_to_foot(X) -> X * 3.
名義型別的宣告和使用方式與其他使用者定義的型別相同。編譯器會識別此語法,但不會執行額外的型別檢查。名義型別的型別檢查是在 Dialyzer 中完成的。
根據名義型別檢查規則,如果兩個名義型別具有不同的名稱,且一個型別未巢狀在另一個型別中,則它們不相容。
例如,如果我們從上面的範例繼續
-spec foo() -> foot().
foo() -> meter_ctor(24).
% Output type: meter().
% Expected type: foot().
% Result: Dialyzer error.
Dialyzer 會針對函式 foo()
傳回以下錯誤
Invalid type specification for function example:foo/0.
The success typing is example:foo
() ->
nominal({'example', 'meter', 0, 'transparent'}, integer())
But the spec is example:foo
() -> foot()
The return types do not overlap
另一方面,名義型別檢查不會強制使用者將值包裝或註解為名義型別。在以下範例中,Dialyzer 不會針對函式 bar()
傳回任何錯誤。meter_to_foot()
的 spec 預期輸入為 meter()
型別。允許傳入 integer()
型別,因為 meter()
定義為具有 integer()
型別。只有當輸入為不同的基本型別時,例如 atom()
,Dialyzer 才會拒絕它。
-spec bar() -> foot().
bar() -> meter_to_foot(24).
% Input type: integer().
% Expected type: meter().
% Result: No error.
當預期為名義型別時,也允許傳入具有相同結構的型別。在以下範例中,Dialyzer 不會針對函式 qaz()
傳回任何錯誤。meter_ctor()
的 spec 預期輸入為 integer()
型別。允許傳入 meter()
型別,因為 meter()
定義為具有 integer()
型別。同樣地,當預期為 integer()
時,也允許傳入 foot()
型別。
-spec qaz() -> integer().
qaz() -> meter_ctor(meter_ctor(24)).
% Input of the outer meter_ctor(): meter().
% Expected type: integer().
% Result: No error.
還值得注意的是,支援巢狀名義型別。在以下範例中
-nominal state() :: integer().
-nominal container() :: state().
-nominal record_container() :: #{a => state(), b => [container()|atom()]}.
Dialyzer 中的名義型別檢查會正確地識別,只要預期為 state()
型別,就可以安全地使用 container()
,就像預期為 integer()
型別時可以安全地使用 state()
一樣。即使 container()
和 state()
具有不同的名稱,也允許使用 [state()]
作為 record_container()
建構的第二個參數。
為了指定名義型別檢查規則,需要定義三個術語。這些定義的範圍限於此 EEP。
對於兩個名義型別 s()
和 t()
,如果 t()
巢狀在 s()
中,則 s()
是 t()
的名義子型別,而 t()
是 s()
的名義超型別。
s()
是 t()
的名義子型別的情況t()
可以直接巢狀在 s()
中。
-nominal s() :: t().
t()
可以巢狀在其他名義型別中,然後巢狀在 s()
中。
-nominal s() :: nominal_1().
-nominal nominal_1() :: nominal_2().
-nominal nominal_2() :: t().
如果非名義型別的結構被型別檢查器視為相容,則該非名義型別與名義或非名義型別相容。
對於 Dialyzer,如果兩個型別共用相同的值,則它們相容。函式 erl_types:t_inf/2
會計算 2 個型別的交集。具有非空交集的兩個型別在結構上是相容的。
integer()
在結構上是相容的。(它們的交集是值 4711。)list(any_type)
和 []
在結構上是相容的。(它們的交集是 []
。)-nominal t() :: integer()
和 4711 在結構上是相容的。(它們的交集是值 t() :: 4711
。)-nominal t() :: non_neg_integer()
和 neg_integer()
在結構上不相容。(沒有值屬於 non_neg_integer()
和 neg_integer()
。)此 EEP 提出的名義型別檢查規則可以總結如下
函式具有 -spec
,指定引數或傳回型別為名義型別 a/0
(或任何其他元數),接受或可能傳回
a/0
a/0
的名義超型別或子型別函式具有 -spec
,指定引數或傳回型別為結構型別 b/0
(或任何其他元數),接受或可能傳回
當預期為名義子型別時,允許超型別的原因有 3 個(即使子型別關係不對稱)
這種名義型別實作的一個好處是它們可以編碼不透明型別,因此 OTP 團隊可以減少維護工作。名義型別的邏輯完全取代了 Dialyzer 中不透明型別的邏輯。此外,此實作使不透明型別的型別檢查以線性時間執行,而不是以四次時間執行。
此更新為不透明型別的效能和程式碼可維護性帶來了許多好處,並且使用者端無需任何變更。
以下清單摘要說明了不透明型別的未變更和已變更的方面,並簡要討論了此更新的理由
目前的實作:https://github.com/lucioleKi/otp/tree/cleanup
不包含不透明型別或不使用 Dialyzer 的程式碼沒有變更。
包含不透明型別且使用 Dialyzer 的程式碼可能會遇到上述變更。
如果其他型別檢查器未實作名義型別檢查,則它們可以像處理 -type
一樣處理 -nominal
。
如果其他型別檢查器選擇實作名義型別檢查,則它們應以與此 EEP 一致的方式實作。目的是確保名義型別保持相同的語義,並以相同的方式在不同的型別檢查器之間進行型別檢查。
本文件置於公共領域或 CC0-1.0-Universal 授權條款之下,以較寬鬆者為準。