Elixir v1.20 发布:现为渐进类型语言
摘要
Elixir v1.20 引入了基于集合论的渐进类型,能够实现类型推断和可验证的缺陷检测,而无需开发者添加注释,这标志着该语言演进的重大里程碑。
<p><a href="https://lobste.rs/s/wq1csk/elixir_v1_20_released_now_gradually_typed">评论</a></p>
查看缓存全文
缓存时间: 2026/06/03 19:48
# Elixir v1.20 发布:现已成为渐进类型语言
来源:https://elixir-lang.org/blog/2026/06/03/elixir-v1-20-0-released/
2022 年,我们宣布了一项工作,旨在为 Elixir 添加集合论类型(https://elixir-lang.org/blog/2022/10/05/my-future-with-elixir-set-theoretic-types/)。2023 年 6 月,我们发表了一篇获奖论文,阐述了 Elixir 的类型系统设计(https://arxiv.org/abs/2306.06391),并表示我们的工作正从研究转向开发(https://elixir-lang.org/blog/2023/06/22/type-system-updates-research-dev/)。
借助 Elixir v1.20,我们完成了第一个开发里程碑:在不引入类型注解的情况下,对每个 Elixir 程序执行类型推断和渐进类型检查。这意味着 Elixir 能更频繁地报告死代码和**已验证的 bug**:那些执行时一定会导致运行时失败的类型违规。Elixir 能在现有程序中高效地发现已验证的 bug,无需引入开发开销,且误报率极低。
在本次公告中,我们将详细阐述类型系统的目标、Elixir 中 `dynamic()` 类型的含义,以及它是如何发现**已验证的 bug** 的。特别地,我们的实现在 "If T: Benchmark for Type Narrowing"(https://github.com/utahplt/ifT-benchmark/tree/main#benchmark-results)基准测试中表现出色。Elixir 通过了 13 个类别中的 12 个,表明它能从普通 Elixir 代码中恢复精确的类型信息,我们利用这些信息在动态类型程序中找到已验证的 bug。
该类型系统的实现得益于 CNRS(https://www.cnrs.fr/)和 Remote(https://remote.com/)的合作。开发工作目前由 Fresha(https://www.fresha.com/)和 Tidewave(https://tidewave.ai/)赞助。
## 类型,在我的 Elixir 中?
我们的目标是引入一个类型系统,它具备以下特性:
- **可靠**——类型系统推断和分配的类型与程序的行为一致
- **渐进**——Elixir 的类型系统包含 `dynamic()` 类型,可在变量或表达式的类型在运行时检查时使用。在没有 `dynamic()` 的情况下,Elixir 的类型系统表现为静态类型系统
- **对开发者友好**——类型通过基本的集合运算(并集、交集、否定)来描述、实现和组合(因此它是集合论类型系统),并附带清晰的错误信息
将类型系统引入现有语言是一项复杂的变更。因此,我们的第一个里程碑是在不引入类型注解的情况下实现类型系统,但仍通过发现死代码和已验证的 bug 为开发者提供价值。这是通过 `dynamic()` 类型实现的,它在 Elixir 中与其他渐进类型语言有着显著区别。让我们来详细分析。
## `dynamic()` 类型
许多渐进类型系统都有 `any()` 类型,从类型系统的角度来看,它通常意味着“什么都可以”,不会报告任何类型违规。而 Elixir 的渐进类型称为 `dynamic()`,它具有两个重要特性:兼容性(compatibility)和窄化(narrowing)。
在静态类型系统中,当你有一个形状为 `integer() or binary()` 的类型并调用一个函数时,该函数必须同时接受这两种类型。然而,由于类型系统无法精确捕捉所有程序的意图,这可能导致误报。例如,考虑以下简单代码:
```elixir
def percentage_or_error(value) when is_integer(value) do
value_or_error =
if value > 1 do
value
else
"not well"
end
# ... 更多代码 ...
if value > 1 do
value_or_error / 100
else
String.upcase(value_or_error)
end
end
```
尽管 `value_or_error` 的类型是 `integer() or binary()`,但运算符 `/` 只接受数字,而 `String.upcase` 只接受二进制/字符串,上述程序是有效的,并且在运行时不会引发异常。然而,类型系统仍然会报告两次违规,因为提供给 `/` 和 `String.upcase` 的类型并非所接受类型的子类型。
虽然上述程序可以写得更好以避免类型违规,但类型系统总会拒绝一些有效程序。如果 Elixir 在现有代码库中引入太多误报,会迅速削弱对类型系统的信任。因此,Elixir 的渐进类型系统将上述 `value_or_error` 变量标记为类型 `dynamic(integer() or binary())`,这意味着该类型在运行时要么是 `integer()` 要么是 `binary()`。
当调用一个带有 `dynamic()` 类型的函数时,Elixir 仅当提供的类型与接受的类型不相交(disjoint)时才会发出类型违规。在上述程序中,尽管 `/` 只期望数字,但 `dynamic(integer() or binary())` 可以是 `integer()`,并且由于接受类型与提供类型不相交,因此没有类型违规。然而,如果将程序改为这样:
```elixir
value_or_error =
if value > 1 do
value
else
"not well"
end
Map.fetch!(value_or_error, :some_key)
```
由于 `Map.fetch!` 期望一个映射数据结构,而 `value_or_error` 在运行时只能是整数或二进制,接受类型和提供类型不相交,从而产生违规。这就是兼容性属性,它解释了 Elixir 如何仅报告**已验证的 bug**。
然而,如果我们一开始无法发现许多 bug,那么仅报告已验证的 bug 就没有意义。我们通过确保 Elixir 的动态类型可以被窄化来解决这个问题。看这段代码:
```elixir
def add_a_and_b(data) do
data.a + data.b
end
```
在上述程序中,`data` 最初是 `dynamic()` 类型。然后我们在加号运算符内将其用作 `data.a` 和 `data.b`,因此 Elixir 会将 `data` 变量精化为具有类型 `%{..., a: number(), b: number()}`,这意味着它是一个包含 `a` 和 `b` 字段(均为数字值)的映射(并且可能还有其他字段,因此有前导的 `...`)。因此,如果你忘记选择 `.b` 字段而写成:
```elixir
def add_a_and_b(data) do
data.a + data
end
```
`data` 会首先被窄化为形状为 `%{..., a: number()}` 的映射,然后尝试将其用作 `number()`,从而引发违规。
换句话说,Elixir 中的 `dynamic()` 类型实际上作为一个范围工作,可以在整个程序中使用时被精化,并且每当类型检查超出范围时就会报告违规。这与其他渐进类型系统形成对比,后者使用动态类型丢弃所有类型信息。
在幕后,我们的类型推断和类型检查算法的行为就像我们将所有参数类型都注解为 `dynamic()` 一样。一旦我们引入用户提供的类型注解,只要不使用 `dynamic()`,Elixir 的类型系统就会像任何静态类型语言一样运行。而每当你跨越静态-动态边界时,我们开发了新技术(https://elixir-lang.org/blog/2023/09/20/strong-arrows-gradual-typing/),确保渐进类型是可靠的,无需额外的运行时检查。
## 守卫(guard)、子句等的类型检查
此版本的大部分工作是向多种语言构造引入类型检查和窄化。让我们看看其中的一些。
对于守卫,我们可以推断出并集、交集和否定:
```elixir
def example(x, y) when is_list(x) and is_integer(y)
```
上述代码正确推断出 `x` 是列表,`y` 是整数。
```elixir
def example({:ok, x} = y) when is_binary(x) or is_integer(x)
```
上述代码推断出 `x` 是二进制或整数,`y` 是一个二元元组,第一个元素是 `:ok`,第二个元素是二进制或整数。
```elixir
def example(x) when is_map_key(x, :foo)
```
上述代码推断出 `x` 是一个具有 `:foo` 键的映射,表示为 `%{..., foo: dynamic()}`。记住前导的 `...` 表示映射可能还有其他键。
```elixir
def example(x) when not is_map_key(x, :foo)
```
上述代码推断出 `x` 是一个没有 `:foo` 键的映射,其类型为 `%{..., foo: not_set()}`。因此,在函数体内访问 `x.foo` 会引发类型违规。
你还可以使用断言数据结构大小的表达式:
```elixir
def example(x) when tuple_size(x) < 3
```
Elixir 会正确跟踪该元组最多有两个元素,因此访问 `elem(x, 3)` 会引发类型违规。对于映射和列表,我们将大小检查转换为空性检查。换句话说,Elixir 可以查看复杂的守卫,推断类型,并使用这些信息在代码中发现 bug。
对于诸如 `case` 和条件表达式这样的构造,Elixir 会使用前面子句的信息来精化后续子句:
```elixir
case System.get_env("SOME_VAR") do
nil -> :not_found
value -> {:ok, String.upcase(value)}
end
```
`System.get_env("SOME_VAR")` 返回 `nil` 或 `binary()`。由于第一个子句匹配 `nil`,类型系统知道 `value` 不能再是 `nil`,因此它必须仅为 `binary()`,从而允许第二个子句也能通过类型检查而不产生违规。跨子句的窄化还有助于类型系统在现有代码库中发现冗余子句和死代码。
此外,我们为标准库中许多处理元组和映射的函数添加了类型。你可以在发行说明(https://github.com/elixir-lang/elixir/releases/tag/v1.20.0)中找到更多细节。
## 编译时间改进
Elixir v1.20 再次改进了编译时间,尤其是在多核机器上运行的应用程序中。尽管 BEAM 语言通常编译效率很高,但我们的综合基准测试现在将 Elixir 的构建工具列为其中最快的(https://github.com/josevalim/langcompilebench)。如果你希望贡献更多示例和场景,请发起讨论,以便我们提供透明的基准测试套件和结果。
它引入了一个新的编译器选项 `:module_definition`,用于指定模块定义是 `:compiled`(默认)还是 `:interpreted`。这可能会改善大型项目的编译时间,并且不会影响写入磁盘的 `.beam` 文件,只会影响 `defmodule` 内部内容的执行方式。你可以通过在 `mix.exs` 中设置 `elixirc_options: [module_definition: :interpreted]` 来启用它。阅读文档了解更多信息(https://elixir.hexdocs.pm/1.20.0/Code.html#put_compiler_option/2)。
## 下一步是什么?
我们面前最大的问题是:Elixir 何时会引入利用集合论类型的新类型签名?正如我最近在 ElixirConf EU 2026 主题演讲(https://youtu.be/Ay-gnCqDw9o?t=2389)中所讨论的,我们在研究和开发方面仍有工作要做。我们只会引入类型签名,前提是:
- 我们对 Elixir v1.20 中类型系统的性能感到满意(并且我们已经做了大量工作(https://elixir-lang.org/blog/2025/12/02/lazier-bdds-for-set-theoretic-types/)来优化它(https://elixir-lang.org/blog/2026/02/26/eager-literal-intersections/)和(https://elixir-lang.org/blog/2026/03/19/lazy-bdds-with-eager-literal-differences/))
- 我们能够高效地实现递归类型
- 我们能够高效地实现参数化类型
- 我们能够高效地实现将映射的键值对作为可枚举类型进行遍历(我们仍在研究可能的解决方案)
一旦这些问题得到解决,我们将开始探索和讨论带类型的结构体定义,最后是类型签名。像往常一样,我们将通过新闻和 Elixir 论坛(https://elixirforum.com/)让社区了解最新动态。
我们感谢每一位尝试发布候选版本、运行基准测试并给予我们反馈的人!请尝试 Elixir v1.20,并记得修复它为你免费发现的所有 bug!
相似文章
Swift type checker 的最新改进
Swift type checker 的最新改进,这是 Swift 编程语言的一个开发者工具。
Flycheck 38:力量与魔法
Flycheck 38 是一个重大版本,增加了对语言服务器的内置 Eglot 支持、内联诊断注释以及应用快速修复代码操作的能力,复兴了 Emacs 的 linting 工具。
稳定Rust的Never类型
Rust在经过两年多的开发后,稳定了其'never'类型,这一特性使得泛型代码更高效,并简化了语言中的类型推断。
类型推断存在可用性问题 (2019)
这篇博文认为,编程语言中的类型推断虽然被广泛采用,但可能通过增加认知负担和妨碍代码理解而导致可用性问题。
Serokell 对 GHC 的工作:依值类型,第5部分
本文详细介绍了 Haskell 的 GHC 编译器在依值类型方面的最新进展,包括 GADTs 中的可见 forall、命名空间指定导入以及其他编译器改进。