6月3日,José Valim签发了Elixir v1.20。这个版本的意义不在新函数或新模块,而在一件事:Elixir正式成为一门渐进式类型(Gradually Typed)语言。从今天起,即使你一行类型注解都不写,编译器也能推断出类型并报出潜在错误;而当你愿意标注类型时,检查精度会进一步提升。
渐进式类型,不是"渐进式注解"
渐进式类型的核心承诺是零注解也有收益。传统渐进类型语言(如TypeScript)的典型路径是:先写无类型的代码,运行没问题,然后逐步补注解。但Elixir的做法更激进——编译器本身在做推断,你不需要额外工具或插件,不需要--type-check开关,类型信息直接融入编译流程。
这意味着两件事:
- 存量代码立刻受益。你仓库里那些2018年写的、没有任何类型注解的模块,v1.20编译时就能被检查出一部分类型错误。
- 注解是可选的精度杠杆。当你对某个函数的输入输出有明确约束时,加注解能让编译器捕获更细的错误,但你不加也不会退回到"完全无检查"的状态。
类型系统的底层:集合论类型
Elixir的类型系统基于集合论类型(Set-theoretic Types)。这不是常见的Hindley-Milner推导,而是把类型当作集合来运算——交集、并集、补集直接对应类型约束的组合。
一个直观的理解:
integer()是所有整数的集合float()是所有浮点数的集合integer() or float()就是这两个集合的并集,即数值类型integer() and not 0是所有非零整数
集合论类型让Elixir能表达比传统类型系统更细的约束,比如"这个参数要么是字符串要么是nil"(string() or nil),或者"返回值是正整数"(integer() and not (0 or neg_integer()))。这些约束在函数签名里可以直接写出来,编译器也能在推断中利用它们。
实际体验:从无注解到有注解
下面用一个具体模块演示渐进式类型的两种工作方式。
无注解,编译器也能抓错
defmodule Order do
def total(items) do
items
|> Enum.map(fn item -> item.price * item.quantity end)
|> Enum.sum()
end
def discount(total, rate) do
total * rate
end
end
这段代码没有一行类型注解。但在v1.20下,如果你调用时传了错误类型:
# 编译器会警告:rate 应该是数值,这里传了字符串
Order.discount(100, "0.2")
编译器通过推断知道total * rate中的*要求两个参数都是数值,因此rate的类型被推断为number()。传入字符串时,编译器在编译期就能发出警告——不需要你标注任何类型。
加注解,精度更高
当你对业务规则有明确约束时,注解让检查更严:
defmodule Order do
@type item :: %{price: float(), quantity: pos_integer()}
@type total :: non_neg_integer()
@spec total(list(item())) :: total()
def total(items) do
items
|> Enum.map(fn item -> item.price * item.quantity end)
|> Enum.sum()
end
@spec discount(total(), float()) :: float()
def discount(total, rate) do
total * rate
end
end
加了@spec之后,编译器能检查更多场景:
- 传入空列表?
list(item())允许空列表,但Enum.map在空列表上返回0,类型兼容。 - 传入
rate为负数?float()包含负浮点数,如果你想限制为0到1之间,需要更细的集合类型(目前标准库尚无0.0..1.0的浮点区间类型,可以用运行时校验补充)。 total()声明为non_neg_integer(),但函数体返回的是浮点数之和——编译器会报出返回类型不匹配。
这就是渐进式类型的杠杆:推断给你基础安全网,注解给你精确安全网。
启用类型检查
v1.20中类型检查默认可能不是最高严格级别。你可以在项目配置中调整:
# config/config.exs 或 mix.exs 中
config :elixir, :type_checking, :strict
或者在编译时通过环境变量控制:
# 严格模式编译整个项目
ELIXIR_TYPE_CHECKING=strict mix compile
建议在CI中用严格模式编译,本地开发用默认级别,避免推断警告干扰日常开发节奏。
上手建议与注意事项
渐进式类型不是银弹,以下是实际落地时的几个判断:
| 场景 | 建议 |
|---|---|
| 存量项目升级到v1.20 | 先用默认级别编译,看推断产生的警告数量。警告太多说明代码中隐式类型依赖较重,逐模块修复,不要一次性开strict |
| 新项目 | 从第一天就给核心模块加@spec,边缘模块可以不加,让推断覆盖 |
| 公共库/SDK | 必须加@spec。库的用户依赖类型信息做调用推断,无注解的库会成为下游类型链的断点 |
| 性能敏感路径 | 类型检查只在编译期运行,不影响运行时性能。但@spec声明的集合论类型越复杂,编译时间越长——对大型项目注意编译耗时变化 |
几个常见坑:
- 集合论类型语法还在完善中。
integer() and not 0这类否定类型目前可以写,但更复杂的嵌套表达式可能有解析限制,遇到报错时简化表达式即可。 - 动态模式匹配与类型检查的张力。Elixir重度依赖模式匹配,而模式匹配本身就是一种运行时类型约束。编译器的类型推断会尽量利用模式匹配信息,但
case的多分支可能产生并集类型,推断结果有时比你预期的更宽。遇到这种情况,在函数头加@spec比在case内部加更有效。 - 与dialyzer的关系。dialyzer是Elixir/Erlang长期使用的静态分析工具,基于success typing。v1.20的新类型系统不是dialyzer的替代,而是编译器内建的一层更严格的检查。两者可以共存,但警告内容可能有重叠,团队需要决定以哪个为主。
检查清单——升级v1.20时做这几件事:
- [ ]
mix deps.update elixir升级到v1.20 - [ ] 用默认级别编译,记录推断警告数量
- [ ] 给最核心的3-5个业务模块加
@spec,观察编译器额外捕获的错误 - [ ] CI中加入
ELIXIR_TYPE_CHECKING=strict mix compile步骤 - [ ] 评估编译耗时变化,如果strict模式编译时间超过团队容忍阈值,降级为警告模式并逐步收紧
Elixir用七年时间走到了渐进式类型这一步。从v1.20开始,类型安全不再是"要么全注解要么全裸奔"的二选一,而是你可以从推断给你的底线出发,按需加注解、按需收紧。对已有Elixir项目来说,这是最低门槛的类型安全入口——升级版本,编译一下,看看推断替你抓出了什么。