前言
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.a 和 data.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 回傳 nil 或 binary(),而第一個子句已經處理了 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):
- 對 v1.20 的類型系統效能滿意嗎?(已經做了大量優化)
- 能高效實現遞迴類型(recursive types)嗎?
- 能高效實現參數化類型(parametric types)嗎?
- 能高效遍歷 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