Elixir v1.20 发布说明 / 新特性

Elixir v1.20 将该语言转变为一种渐进式类型语言。其核心要点是,Elixir 编译器现在可以进行类型推断,并在无需开发者为现有代码添加任何类型注解的情况下,检查程序中是否存在“已验证的错误”——即保证会在运行时失败的类型违规行为。

基于集合论的类型系统

Elixir 的新类型系统旨在实现健全性、渐进性和开发者友好性。它利用集合论类型,这意味着类型是由基础集合操作组成的:并集、交集和补集。

dynamic() 类型与错误检测

与许多渐进式类型系统中常见的 any() 类型(通常会禁用类型检查)不同,Elixir 的 dynamic() 类型保留了类型信息。它具有两个关键属性:兼容性收敛性

  • 兼容性: 只有当提供的类型与接受的类型完全不相交时,才会报告类型违规。如果存在任何重叠(交集),则认为代码是兼容的。这可以防止在处理动态代码时出现静态类型系统通常会面临的大量误报。
  • 收敛性: 类型系统可以在使用过程中细化变量的类型。例如,如果一个变量被用作 map key 访问(data.a),编译器会将 data 的类型收敛为包含 key :a 的 map。如果随后的代码尝试将该变量作为整数使用,编译器将标记为违规,因为收敛后的类型(一个 map)与预期类型(一个整数)是不相交的。

Guard 与 Clause 中的类型推断

Elixir v1.20 在多个核心语言结构中实现了类型收敛和检查:

Guards

编译器现在可以从 guards 中推断类型。例如,when is_list(x) 推断出 x 是一个 list,而 when not is_map_key(x, :foo) 推断出 x 是一个不包含 key :foo 的 map。它还可以跟踪数据结构的大小;例如 when tuple_size(x) < 3 这样的 guard 确保编译器知道该 tuple 有至多两个元素,从而防止无效的 elem/1 调用。

Case 语句与条件判断

类型信息会在各个 clause 之间进行细化。在 case 语句中,如果第一个 clause 匹配 nil,编译器就会知道在所有后续的 clause 中,该变量不再可能是 nil。这种收敛性允许编译器发现冗余的 clause 并识别现有代码库中的死代码。

编译性能与工具

Elixir v1.20 引入了编译速度的改进,特别是针对多核机器。根据合成基准测试,Elixir 的构建工具现在是 BEAM 语言中最快的。

此外,一个新的编译器选项 :module_definition 允许开发者将模块定义设置为 :interpreted(通过在 mix.exs 中设置 elixirc_options: [module_definition: :interpreted])。这可以在不影响写入磁盘的最终 .beam 文件的情况下,减少大型项目的编译时间。

未来路线图

虽然 v1.20 提供了无需注解的类型推断,但 Elixir 团队仍在研究和开发显式类型签名。只有在满足以下标准后,才会引入这些功能:

  • v1.20 中类型系统的性能得到了验证。

  • 递归类型的有效实现。

  • 参数化类型的有效实现。

  • 作为可枚举对象高效地遍历 map key-value 对。

社区观点

社区对此次发布的反馈褒贬不一,但总体上是积极的。一些开发者称赞了更新带来的“免费”错误检测功能,一位用户指出:

"It's very nice updating Elixir, having no breaking changes across my many projects and it then the compiler just finds bugs for free."

相反,一些批评者认为,向一种最初并非为类型化设计的语言添加类型会比 Gleam 或 Rust 等语言效果更差。其他人则质疑这种方法与 Dialyzer 的 "success typing" 如何比较,以及渐进式类型系统是否会影响程序的渐进性能。

参考资料

Sources