作者
Isabell Huang <isabell(at)erlang(dot)org>
狀態
已接受
類型
標準追蹤
建立日期
2024-03-18

EEP 69:名義型別 #

摘要 #

此 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 個型別的交集。具有非空交集的兩個型別在結構上是相容的。

  • 範例
    • 4711 和 42 在結構上不相容。(沒有整數值可以是 4711 和 42。)
    • 4711 和 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 個(即使子型別關係不對稱)

  • 為了盡量減少使用者將值轉換為名義型別或重寫規格的精力。
  • Dialyzer(成功型別)允許在結構型別之間這樣做。Erlang 的大多數現有型別檢查器也允許這樣做。
  • 使名義型別檢查比限制更具彈性。

最佳化不透明型別的型別檢查 #

這種名義型別實作的一個好處是它們可以編碼不透明型別,因此 OTP 團隊可以減少維護工作。名義型別的邏輯完全取代了 Dialyzer 中不透明型別的邏輯。此外,此實作使不透明型別的型別檢查以線性時間執行,而不是以四次時間執行。

此更新為不透明型別的效能和程式碼可維護性帶來了許多好處,並且使用者端無需任何變更。

以下清單摘要說明了不透明型別的未變更和已變更的方面,並簡要討論了此更新的理由

無需使用者變更 #

  • 編譯器對不透明型別的處理方式不變。
  • 即使不透明型別在 Dialyzer 中根據名義型別進行編碼,它們仍保留其語義。
  • 不透明型別的文件仍然隱藏。

新功能 #

  • 現在,對不透明型別進行模式比對會產生警告,而不會停止 Dialyzer 的分析。(以前,對不透明型別進行模式比對會產生錯誤並導致 Dialyzer 停止分析,因此 Dialyzer 無法捕獲其他錯誤。)
  • 完全支援對巢狀不透明型別進行型別檢查,就像對名義型別一樣。

好處 #

  • 效能:先前,不透明型別在 Dialyzer 內最多具有四次分析時間。現在,它們最多具有線性分析時間。
  • 可維護性:名義型別檢查不會為 Dialyzer 引入特殊情況。現在可以移除所有專門針對不透明型別的額外型別檢查函式。
  • 模式比對:Dialyzer 對不透明型別的分析不會因模式比對違規而硬停止,這會產生更精確的警告,因為分析會繼續。
  • 巢狀不透明型別:檢查巢狀不透明型別的功能與名義型別一樣有用。
    對不透明型別而言。

參考實作 #

目前的實作:https://github.com/lucioleKi/otp/tree/cleanup

回溯相容性 #

不包含不透明型別或不使用 Dialyzer 的程式碼沒有變更。

包含不透明型別且使用 Dialyzer 的程式碼可能會遇到上述變更。

對於其他型別檢查器 #

如果其他型別檢查器未實作名義型別檢查,則它們可以像處理 -type 一樣處理 -nominal

如果其他型別檢查器選擇實作名義型別檢查,則它們應以與此 EEP 一致的方式實作。目的是確保名義型別保持相同的語義,並以相同的方式在不同的型別檢查器之間進行型別檢查。

著作權 #

本文件置於公共領域或 CC0-1.0-Universal 授權條款之下,以較寬鬆者為準。