作者
Ilya Klyuchnikov <ilya(点)klyuchnikov(在)gmail(点)com>
状态
最终/26.0 在 OTP 26 版本中实现
类型
标准跟踪
创建于
2023-03-08
发布历史
https://github.com/erlang/eep/pull/44

EEP 61:内置动态类型 #

摘要 #

此 EEP 提议一个新的内置类型 dynamic()(除了现有的内置类型 term())以使 Erlang 对渐进式类型检查更加友好。拥有一个用于渐进式类型的特殊动态类型是一种被广泛采用的工业解决方案。

渐进式类型和动态类型 #

Erlang 是许多编程语言中的一种,它最初是一种纯粹的动态类型语言,没有静态类型信息,但后来获得了扩展(或方言)以在源代码中指定类型信息(类型提示、规范)。

Erlang 通过 EEP 8(类型和函数规范)进行了扩展。

当前关于类型和规范的文档可以在 https://erlang.ac.cn/doc/reference_manual/typespec.html 找到。

除了用于文档目的外,类型和规范还可以被工具用于静态分析。现有的工具包括

Dialyzer 在成功类型化的范式中使用类型规范(在 Pull Request 6281 “记录规范的含义” 和原始 论文 “一种用于在 Erlang 中指定类型契约及其与成功类型化的交互的语言” 中有记录)。值得注意的是,dialyzer 处理规范的方式与传统的类型检查器非常不同。

eqWAlizer 使用类型规范以传统方式执行类型检查。

到目前为止,将静态类型化采用或改造到动态类型语言中的主要工业方法是通过渐进式类型化(https://en.wikipedia.org/wiki/Gradual_typinghttp://samth.github.io/gradual-typing-bib)。渐进式类型化具有特殊的类型 dynamic?,它允许以用户友好的方式混合无类型(或动态类型化)和静态类型化的代码,并且还有助于在大型项目中逐步采用静态类型化。拥有用于渐进式类型化的特殊动态类型是一种被广泛采用的解决方案。

由于 Erlang 在表面语法中没有内置的动态类型,因此 eqWAlizer 不得不引入自己的类型 eqwalizer:dynamic(),它具有用于渐进式类型化的特殊语义。

term()/any() 和通过示例的子类型化 #

在开始讨论动态类型之前,值得澄清内置 Erlang 类型 term()any() 的含义和语义。

  • any() 是子类型格的顶层类型元素。这在 Erlang 文档 中有明确说明。
  • term()any() 的别名。它们是相同的类型。在本文档的其余部分,我们将使用 term(),因为它更具 Erlang 特性。

类型 term() 类似于其他语言和类型检查器中的以下类型

从传统的静态类型化的角度来看,在需要更具体的类型的地方使用 term() 值是一种类型错误。

让我们来看一些示例。

示例 1. Erlang,eqWAlizer

-module(example).

-spec foo(term()) -> number().
foo(N) ->
    N.
    ^ % type error. expected: number(), got: term()

-spec n(number()) -> number().
n(N) -> N.

-spec foo_n(term()) -> number().
foo_n(N) ->
    n(N).
      ^ type error. expected: number(), got term()

示例 2. Python,mypy

def foo(n: object) -> int:
    return n
           ~ Incompatible return value type (got "object", expected "int")

def n_fun(n: int) -> int:
    return n

def foo_n(n: object) -> int:
    return n_fun(n)
                 ~ Argument 1 to "n_fun" has incompatible type "object";
                                              expected "int"  [arg-type]

示例 3. TypeScript

function foo(x: unknown): number {
    return x;
    ~~~~~~~~~ Type 'unknown' is not assignable to type 'number'.(2322)
}

function n_fun(n: number): number {
    return n;
}

function foo_n(n: unknown): number {
    return n_fun(n);
                 ~ Argument of type 'unknown' is not assignable
                                  to parameter of type 'number'
}

dynamic() 类型 #

eqWAlizer 有一个特殊的类型 eqwalizer:dynamic(),其文档 在此

该类型类似于其他语言和类型检查器中的以下类型

  • Python 中的 Any(mypy、PyRight、pyre 以相同的方式处理它)
  • TypeScript 中的 any
  • Ruby/rbs 中的 untyped
  • Ruby/Sorbet 中的 untyped
  • luau 中的 any
  • Hack 中的 dynamic
  • Flow 中的 any

eqWAlizer 从 Hack 借用了这个名称。命名选择是由以下考虑因素决定的

  • eqwalizer:any() 会因为内置的 Erlang 类型 any() 而产生误导和混淆
  • eqwalizer:dynamic() 通常用于标记“固有动态代码”:从 ETS 读取、消息传递、反序列化等。

(在本文档的其余部分中,我们仅使用 dynamic() 作为 eqwalizer:dynamic() 的缩写。)

引入 dynamic() 类型的基本原理在 eqWAlizer 文档 和其他语言和类型检查器的已提及资源中给出。

我们建议使用新的 dynamic() 内置类型扩展 Erlang 表面语法,以使 Erlang 更容易使用基于渐进式类型化(如 eqWAlizer 或 Gradualizer)的工具,并简化其采用(包括将其用于类型化 OTP 库)。

可以只用一种类型吗? #

有人可能会争辩说,只用一种类型 - term()/any() 就足够了,并且由工具将其解释为适当的顶层类型或作为用于渐进式类型化的特殊动态类型。然而,从长远来看,这将是有限制的。

其他类似语言和类型检查器的故事表现出一种共同点:它们最终都具有用于不同严格程度和保证的选项或模式。或者,粗略地说 - 它们至少有两种模式:渐进模式和严格模式。渐进模式针对非侵入式增量采用进行了优化,而严格模式针对类型安全和强大的可靠保证进行了优化。

  • 在足够大的项目中,会出现具有严格类型化的地方和具有“大多数动态类型化”的地方。
  • 区分 term()dynamic() 而不引入歧义或混淆变得很重要。
  • 类型语言并不总是足够具有表现力来写下精确的类型。在这种情况下,dynamic() 类型充当逃生舱口。
  • 在一些关键任务应用程序中,正确性最为重要,人们可能希望使用带有详细检查/保护的 term() 类型,以确保没有任何东西逃脱类型检查。
  • Gradualizer 的开发中,仅使用 term()/any() 在实践中也被证明是有问题的。最初,Gradualizer 将 any() 用于动态类型,并将 term() 用作顶层类型。根据 Gradualizer 作者的说法,事实证明这很令人困惑,并且与这些类型的预先存在的使用不兼容。

这对 dialyzer 意味着什么? #

在内部,dialyzer 已经将 any()/term() 用作相对于规范的 dynamic()

一个例子

-module(example).
-export([get_foo/1, get_bar/1, get_moo/1]).

-record(rec, {
  foo :: term() | atom(),
  bar :: number() | atom(),
  moo :: term()
}).

-spec get_foo(#rec{}) -> number().
get_foo(R) -> R#rec.foo.

-spec get_bar(#rec{}) -> number().
get_bar(R) -> R#rec.bar.

-spec get_moo(#rec{}) -> number().
get_moo(R) -> R#rec.moo.

运行 dialyzer (dialyzer example.erl) 会产生输出

example.erl:14:2: The success typing for example:get_bar/1 implies
                  that the function might also return atom()
                  but the specification return is number()

至于 eqwalizer:dynamic() - 它被 定义any()/term() 的别名,并且可以与 dialyzer 一起使用。(然而,以这种方式定义它会给迂腐和好奇的用户造成困惑。)

参考实现 #

https://github.com/erlang/otp/pull/6993

向后兼容性 #

借助 PR 6335(允许局部重新定义内置类型),此更改将在 OTP 26 中向后兼容。

版权 #

本文档放置在公共领域或 CC0-1.0-Universal 许可证下,以更宽松的为准。