Elixir 1.20 正式發布:正式晉升為「漸進式型別語言」
2026 年 6 月 3 日 · 作者:José Valim · 分類:公告
2022 年,我們宣布了要為 Elixir 加上「集合論型別系統」(set-theoretic types)的計畫。2023 年 6 月,我們發表了一篇獲獎論文,介紹 Elixir 型別系統的設計理念,並宣布研究階段告一段落,準備進入實際開發。
到了 Elixir v1.20,我們達成了第一個開發里程碑:能對「每一個 Elixir 程式」進行型別推論(type inference)並做漸進式型別檢查(gradually type check),而且不需要開發者手寫任何型別標註。這代表 Elixir 現在可以主動回報「死代碼」(dead code)與「已驗證的 bug」(verified bugs)—— 也就是那些只要執行就保證會爆的型別違規。Elixir 能在不增加開發者負擔的前提下,有效找出既有程式中的 verified bug,而且誤判率極低。
在這篇公告裡,我們會拆解型別系統的設計目標、解釋 dynamic() 型別在 Elixir 裡是什麼意思,以及它怎麼找出 verified bug。值得一提的是,我們的實作在「If T: Benchmark for Type Narrowing」這個基準測試中表現優異,Elixir 通過了 13 個類別中的 12 個,代表它能從一般 Elixir 程式碼中還原出精確的型別資訊,我們就用這個能力在動態型別程式裡找出 verified bug。
這個型別系統能夠問世,要感謝 CNRS 與 Remote 公司的合作夥伴關係。目前的開發工作則由 Fresha 與 Tidewave 贊助。
型別,出現在我的 Elixir 裡?
我們的目標是建立一個符合以下特質的型別系統:
- 健全(sound):型別系統推論與指派的型別,必須與程式的實際行為一致
- 漸進(gradual):Elixir 的型別系統包含
dynamic()型別,當變數或運算式的型別只能在 runtime 檢查時,就用它。在沒有dynamic()的地方,Elixir 的型別系統就像一般的靜態型別系統一樣運作 - 開發者友善(developer friendly):型別用基本的集合運算描述、實作與組合 —— 聯集(union)、交集(intersection)、差集(negation),所以這是一個「集合論型別系統」,錯誤訊息也相當清楚
在一個既有的語言裡導入型別系統是個複雜的大工程。因此我們的第一個里程碑是:先把型別系統做出來,但還不引入型別標註語法,僅靠它找出死代碼與 verified bug 就已經對開發者有價值了。這靠的是 dynamic() 型別 —— 在 Elixir 裡,dynamic() 代表「這個值的型別只能等到執行時才知道」。
要達到這個里程碑,型別系統的精確度必須非常高。如果在現有程式碼裡產生一堆誤判(false positives),很快就會侵蝕掉開發者對型別系統的信任。為此,Elixir 的漸進式型別系統會把上面那個 value_or_error 變數標記為 dynamic(integer() or binary()),代表它在 runtime 只能是 integer 或 binary。
當呼叫一個接受 dynamic() 型別的函式時,Elixir 只會在「傳入型別」與「接受型別」彼此互斥(disjoint)時,才發出型別違規警告。以上面的例子來說,雖然 / 只接受數字,但 dynamic(integer() or binary()) 在 runtime 有可能是 integer,而「接受型別」與「供給型別」並沒有互斥,所以不會發出警告。但如果程式改成這樣:
value_or_error =
if value > 1 do
value
else
"not well"
end
Map.fetch!(value_or_error, :some_key)
因為 Map.fetch! 預期一個 map 結構,但 value_or_error 在 runtime 只能是 integer 或 binary,兩者完全互斥,這時候就會爆出型別違規。這就是所謂的「相容性原則」(compatibility property),也是 Elixir 只回報 verified bug 的原因。
不過,只回報 verified bug 卻找不到幾個 bug 的話,這套型別系統也沒什麼用。我們用「讓 dynamic() 型別可以 narrow(縮窄)」來解決這問題。看看這段程式:
def add_a_and_b(data) do
data.a + data.b
end
在上面這段程式裡,data 一開始是 dynamic() 型別。我們在加法運算子裡用到了 data.a 與 data.b,所以 Elixir 會把 data 這個變數 refine 成 %{..., a: number(), b: number()} —— 也就是一個 map,至少有 a 跟 b 兩個欄位是 number 值(前面的 ... 代表可能還有其他欄位)。因此,如果你不小心忘了選 .b 欄位,寫成這樣:
def add_a_and_b(data) do
data.a + data
end
data 會先被 narrow 成 %{..., a: number()} 的 map,接著又嘗試被當成 number() 使用,這時候就會爆出型別違規。
換句話說,dynamic() 型別正是 Elixir 型別系統的「漸進」核心:它讓我們在不改變既有動態語意的前提下,逐步引入靜態型別檢查的價值。
編譯時間改善
Elixir v1.20 也再次改善了編譯時間,特別是在多核心機器上執行的應用。雖然 BEAM 語言的編譯本來就很快,但我們的合成基準測試現在把 Elixir 的建置工具列為「BEAM 語言中最快的」。如果你想貢獻更多情境與範例,歡迎開個討論串讓我們提供一套透明的基準測試。
這個版本同時引入了一個新的編譯器選項 :module_definition,用來指定 module 定義要以 :compiled(預設)或 :interpreted 模式處理。在大型專案中這能改善編譯時間,而且它不會影響寫到磁碟的 .beam 檔案,只影響 defmodule 內部程式碼的執行方式。你可以在 mix.exs 設成 elixirc_options: [module_definition: :interpreted] 來開啟它。詳情請看官方文件。
接下來呢?
眼前最大的問題是:Elixir 什麼時候會引入「真正利用集合論型別的型別簽名(type signatures)」?就像我最近在 ElixirConf EU 2026 開幕演講(keynote)提到的,我們前面還有研究與開發工作要完成。我們只有在滿足以下條件時,才會正式引入型別簽名:
- 我們對 v1.20 的型別系統效能感到滿意(我們已經做了大量效能最佳化)
- 我們能有效率地實作「遞迴型別」(recursive types)
- 我們能有效率地實作「參數化型別」(parametric types)
- 我們能有效率地實作「遍歷 map 的 key-value pair」當作 enum 操作
在接下來幾個月,隨著 Elixir v1.20 漸漸被社群採用,José Valim 與 Elixir 核心團隊也會持續收集回饋、修正早期採用者遇到的問題,型別系統的下一步細節會在後續公告揭曉。
要了解更多,請看 v1.20 的 release notes,並升級你專案的 Elixir 版本試試看這套漸進式型別檢查。
網友熱門留言 (5)