← 返回 Siami 首頁

Elixir 1.20 正式發布:宣告進入漸進式型別語言時代

▲ 349 💬 114
Elixir 1.20 正式發布:宣告進入漸進式型別語言時代

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)

#1 Hacker News ▲ 180
我本來想用 functional programming 做專案,Elixir 缺乏靜態型別差點讓我放棄。最後我還是用了,雖然有時會想念靜態型別,但 immutable 加上不用處理繼承這些 OO 抽象化,讓這筆交易很值得。
#2 Hacker News ▲ 95
如果你用 Phoenix,在資料模型層用 changesets 做型別檢查、再把這套驗證一路串到 UI,是個很好的折衷。Changesets 本身就內建型別驗證。
#3 Hacker News ▲ 62
Ecto Changesets 算是 runtime 建構,類似 Python 的 Pydantic。對了,Typescript 圈也有 Effect Schema,這個 pattern 我也很愛——雖然在大資料集上 runtime 成本不低。
#4 Hacker News ▲ 41
如果只願意用具備相同功能的語言,那學習就沒意義了。看一個不同典範的語言怎麼在沒有型別的情況下 manage,反而更能啟發思考。
#5 Hacker News ▲ 73
我也試過 Gleam——它是另一個選擇。但說真的,Elixir 的型別系統這次走 set-theoretic + gradual 的路線,我覺得對 Elixir 既有的 dynamic 哲學是更對的延伸,不像 Gleam 幾乎變成另一個 ML 系語言。