作者
Ilya Klyuchnikov <ilya(dot)klyuchnikov(at)gmail(dot)com>
狀態
最終版本/26.0 已在 OTP 26 版本中實作
類型
標準追蹤
建立於
2023-03-08
發布歷史
https://github.com/erlang/eep/pull/44

EEP 61:內建動態類型 #

摘要 #

此 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_typinghttp://samth.github.io/gradual-typing-bib)。漸進式類型化具有特殊的類型 dynamic?,它允許以使用者友善的方式混合未類型化(或動態類型化)和靜態類型化的程式碼,並且也有助於在大型專案中逐步採用靜態類型化。針對漸進式類型使用特殊的動態類型是一種廣泛採用的解決方案。

由於 Erlang 在表面語法中沒有內建的動態類型,eqWAlizer 必須引入自己的類型 eqwalizer:dynamic(),並為漸進式類型化設定特殊的語意。

term()/any() 和透過範例進行子類型化 #

在開始討論動態類型之前,值得釐清內建 Erlang 類型 term()any() 的意義和語意。

  • any() 是子類型格的頂層類型元素。這在 Erlang 文件 中明確指出
  • term()any() 的別名。它們是相同的類型。在本文檔的其餘部分,我們將使用 term(),因為它更具 Erlang 特性。

term() 類型與其他語言和類型檢查器中的以下類型類似

從傳統靜態類型的觀點來看,在預期使用更具體的類型的地方使用 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(),其文件位於 此處

此類型與其他語言和類型檢查器中的以下類型類似

  • Python 中的 Any(mypy、PyRight、pyre 以相同方式處理它)
  • TypeScript 中的 any
  • Ruby/rbs 中的 untyped
  • Ruby/Sorbet 中的 untyped
  • luau 中的 any
  • Hack 中的 dynamic
  • Flow 中的 any

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() 類型,以確保沒有任何內容逃脫類型檢查。
  • 事實證明,在 Gradualizer 的開發中,僅使用 term()/any() 也會產生問題。最初,Gradualizer 將 any() 用於動態類型,並將 term() 用作頂層類型。根據 Gradualizer 作者的說法,這證明會造成混淆,並且與這些類型的先前使用不相容。

這對 dialyzer 意味著什麼? #

在內部,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-通用授權之下,以更寬鬆者為準。