前言

2022 年,Elixir 團隊宣布要為這個語言加入「集合論類型系統」(set-theoretic type system)。經過四年的研發,由 CNRS 與 Remote 合作、Fresha 和 Tidewave 贊助,這個願景終於在 Elixir v1.20 中實現了第一個重要里程碑。

這篇文章由 Elixir 創始人 José Valim 親自撰寫,我來幫你整理重點,用比較輕鬆的方式看懂這次更新到底有什麼了不起。


什麼是「漸進式類型系統」?

簡單來說,Elixir v1.20 現在可以對每一個 Elixir 程式進行類型推論和漸進式類型檢查,而且不需要你寫任何類型註解(type annotations)。

這意味著什麼呢?

  • Elixir 現在會自動幫你找出死程式碼(dead code)
  • 會找出已驗證的 bug(verified bugs)—— 這些是如果執行到就一定會在執行時期失敗的類型錯誤
  • 不需要你加註解、不會增加開發者負擔、誤報率極低

換句話說,你什麼都不用改,Elixir 就免費送你一堆潛在 bug 讓你修。


Elixir 的 dynamic() 類型:跟其他語言的 any() 不一樣

很多漸進式類型系統(比如 TypeScript)都有類似 any 的類型,意思是「什麼都可以,不做檢查」。但 Elixir 的漸進式類型叫做 dynamic(),而且有兩個重要特性:

1. 相容性(Compatibility)

dynamic() 類型在呼叫函數時,只有當「傳入的類型」和「函數接受的類型」完全不相交(disjoint)時,才會報錯。

舉個例子:

value_or_error =
  if value > 1 do
    value
  else
    "not well"
  end

Map.fetch!(value_or_error, :some_key)

這裡 value_or_error 的類型是 dynamic(integer() or binary())。而 Map.fetch! 只接受 map。因為 integer 和 binary 跟 map 完全不重疊,所以會報一個已驗證的 bug

但如果是這樣:

if value > 1 do
  value_or_error / 100  # value_or_error 是 dynamic(integer() or binary())
else
  String.upcase(value_or_error)
end

/ 只接受數字,但 dynamic(integer() or binary()) 有可能是 integer,所以不報錯。String.upcase 同理。這就是相容性——只抓「一定錯」的 bug,不抓「可能對」的情況。

2. 類型縮小(Narrowing)

dynamic() 不是死板的一成不變,它會隨著程式執行被「縮小」:

def add_a_and_b(data) do
  data.a + data.b
end

data 一開始是 dynamic(),但因為你用到了 data.adata.b,Elixir 會把 data 推論成 %{..., a: number(), b: number()} 的 map 類型。

如果你後來不小心寫成 data.a + data,Elixir 會發現 data 被縮小成 map 之後又被當成 number 使用,於是報錯。

用一句話總結:dynamic() 在 Elixir 中就像一個「範圍」,隨著程式碼的使用不斷被縮小,超出範圍就報錯。這跟其他語言用 dynamic 丟棄所有類型資訊的做法完全不同。


類型檢查 Guards、子句和更多

v1.20 的類型系統支援了好幾個新的語法結構:

Guards 類型推論

def example(x, y) when is_list(x) and is_integer(y) do
  # x 被推論為 list,y 被推論為 integer
end
def example(x) when not is_map_key(x, :foo) do
  # x 被推論為 %{..., foo: not_set()}
  # 所以 x.foo 會報類型錯誤
end
def example(x) when tuple_size(x) < 3 do
  # x 被推論為最多 2 個元素的 tuple
  # elem(x, 3) 會報錯
end

Elixir 現在可以從複雜的 guards 中推論出精確的類型資訊。

case 子句的類型縮小

case System.get_env("SOME_VAR") do
  nil -> :not_found
  value -> {:ok, String.upcase(value)}
end

因為 System.get_env/1 回傳 nilbinary(),而第一個子句已經處理了 nil,所以第二個子句的 value 被縮小為 binary()String.upcase/1 自然不會報錯。

這種跨子句的類型縮小也能幫助找出冗餘的子句和死程式碼。


編譯速度又提升了

除了類型系統,v1.20 還有幾個實用更新:

  • 多核心機器的編譯速度大幅改善,合成基準測試顯示 Elixir 的 build tool 現在是 BEAM 語言中最快的
  • 新增編譯器選項 :module_definition,可以設為 :compiled(預設)或 :interpreted。在大型專案中,設為 :interpreted 可能進一步提升編譯速度。在 mix.exs 中設定:
elixirc_options: [module_definition: :interpreted]

接下來會發生什麼?

José Valim 在 ElixirConf EU 2026 的主題演講中提到,Elixir 團隊還在研究以下幾個問題,解決後才會正式引入類型簽名(type signatures):

  1. 對 v1.20 的類型系統效能滿意嗎?(已經做了大量優化)
  2. 能高效實現遞迴類型(recursive types)嗎?
  3. 能高效實現參數化類型(parametric types)嗎?
  4. 能高效遍歷 map 的 key-value pair 作為 enumerable 嗎?(還在研究中)

一旦這些問題都解決了,Elixir 就會開始探索 typedstruct 定義和完整的類型簽名。


總結

Elixir v1.20 的最大亮點就是漸進式類型系統。它不需要你寫任何註解,就能自動幫你找出潛在的類型 bug,而且誤報率極低。這對於大型專案來說,等於多了一個不用維護的 lint tool。

如果你正在用 Elixir,建議升級試試看,讓它幫你找出免費的 bug 吧!

參考來源:Elixir v1.20 released: now a gradually typed language by José Valim