本 EEP 描述了 Erlang 语言的扩展,用于声明 Erlang 项的集合以形成特定类型,有效地形成所有 Erlang 项集合的特定子类型。随后,这些类型可用于指定记录字段的类型以及函数的参数和返回值。
类型信息可用于记录函数接口,为诸如 Dialyzer 之类的错误检测工具提供更多信息,并可被诸如 Edoc 之类的文档工具利用,以生成各种形式的程序文档。预计本文档中描述的类型语言将取代并最终替换 Edoc 使用的纯粹基于注释的 @type 和 @spec 声明。
类型描述了 Erlang 项的集合。类型由一组预定义的类型(例如,integer()
、atom()
、pid()
、…)构成和构建,如下所述。预定义的类型表示属于此类型的通常无限的 Erlang 项集。例如,类型 atom()
代表所有 Erlang 原子集的。
对于整数和原子,我们允许单例类型(例如,整数 -1
和 42
或原子 'foo'
和 'bar'
)。
所有其他类型都使用预定义类型或单例类型的并集来构建。在类型和其子类型之间的类型并集中,子类型被超类型吸收,并且该并集随后被视为子类型不是该并集的组成部分。例如,类型并集
atom() | 'bar' | integer() | 42
描述的项集合与类型并集相同
atom() | integer()
由于类型之间存在子类型关系,因此类型形成一个格,其中最顶层元素 any()
表示所有 Erlang 项的集合,而最底层元素 none()
表示空的项集合。
下面给出了预定义类型的集合和类型的语法
Type :: any() %% The top type, the set of all Erlang terms.
| none() %% The bottom type, contains no terms.
| pid()
| port()
| ref()
| [] %% nil
| Atom
| Binary
| float()
| Fun
| Integer
| List
| Tuple
| Union
| UserDefined %% described in Section 2
Union :: Type1 | Type2
Atom :: atom()
| Erlang_Atom %% 'foo', 'bar', ...
Binary :: binary() %% <<_:_ * 8>>
| <<>>
| <<_:Erlang_Integer>> %% Base size
| <<_:_*Erlang_Integer>> %% Unit size
| <<_:Erlang_Integer, _:_*Erlang_Integer>>
Fun :: fun() %% any function
| fun((...) -> Type) %% any arity, returning Type
| fun(() -> Type)
| fun((TList) -> Type)
Integer :: integer()
| Erlang_Integer %% ..., -1, 0, 1, ... 42 ...
| Erlang_Integer..Erlang_Integer %% specifies an integer range
List :: list(Type) %% Proper list ([]-terminated)
| improper_list(Type1, Type2) %% Type1=contents, Type2=termination
| maybe_improper_list(Type1, Type2) %% Type1 and Type2 as above
Tuple :: tuple() %% stands for a tuple of any size
| {}
| {TList}
TList :: Type
| Type, TList
由于列表是常用的,因此它们具有简写类型表示法。类型 list(T)
的简写为 [T]
。简写 [T,...]
表示元素类型为 T
的非空正确列表的集合。两个简写的唯一区别在于 [T]
可以是空列表,但 [T,...]
不能。
请注意,list()
的简写(即,未知类型元素的列表)是 [_]
(或 [any()]
),而不是 []
。符号 []
指定空列表的单例类型。
为了方便起见,还内置了以下类型。可以将它们视为表中也显示的类型并集的预定义别名。(下面的一些类型并集略微滥用了类型的语法。)
========================== =====================================
Built-in type Stands for
========================== =====================================
``term()`` ``any()``
``bool()`` ``'false' | 'true'``
``byte()`` ``0..255``
``char()`` ``0..16#10ffff``
``non_neg_integer()`` ``0..``
``pos_integer()`` ``1..``
``neg_integer()`` ``..-1``
``number()`` ``integer() | float()``
``list()`` ``[any()]``
``maybe_improper_list()`` ``maybe_improper_list(any(), any())``
``maybe_improper_list(T)`` ``maybe_improper_list(T, any())``
``string()`` ``[char()]``
``nonempty_string()`` ``[char(),...]``
``iolist()`` ``maybe_improper_list(``
``char() | binary() |``
``iolist(), binary() | [])``
``module()`` ``atom()``
``mfa()`` ``{atom(),atom(),byte()}``
``node()`` ``atom()``
``timeout()`` ``'infinity' | non_neg_integer()``
``no_return()`` ``none()``
========================== =====================================
不允许用户定义与预定义或内置类型名称相同的类型。这由编译器检查,违反此规则会导致编译错误。(为了引导目的,如果涉及刚引入的内置类型,也可能只会导致警告。)
注意:还存在以下内置列表类型,但预计很少使用。因此,它们的名字很长
nonempty_maybe_improper_list(Type) :: nonempty_maybe_improper_list(Type, any())
nonempty_maybe_improper_list() :: nonempty_maybe_improper_list(any())
其中以下两种类型
nonempty_improper_list(Type1, Type2)
nonempty_maybe_improper_list(Type1, Type2)
定义人们期望的 Erlang 项集合。
为了方便起见,我们允许使用记录表示法。记录只是相应元组的简写
Record :: #Erlang_Atom{}
| #Erlang_Atom{Fields}
记录已扩展为可能包含类型信息。这在下面的第 3 节中描述。
如所见,类型的基本语法是一个原子,后跟闭合的括号。使用 'type'
编译器属性声明新类型,如下所示
-type my_type() :: Type.
其中类型名称是原子(上面的 'my_type'
),后跟括号。类型是上一节中定义的类型。当前的一个限制是类型只能包含预定义的类型或先前定义的用户定义的类型。编译器会强制执行此限制,并导致编译错误。(记录目前存在类似的限制)。
这意味着无法定义通用的递归类型。解除此限制是未来的工作。
类型声明也可以通过在括号之间包含类型变量来参数化。类型变量的语法与 Erlang 变量相同(以大写字母开头)。当然,这些变量可以(并且应该)出现在定义的 RHS 上。下面是一个具体的例子
-type orddict(Key, Val) :: [{Key, Val}].
可以在记录声明中指定记录字段的类型。其语法如下
-record(rec, {field1 :: Type1, field2, field3 :: Type3}).
对于没有类型注释的字段,它们的类型默认为 any()
。也就是说,上面的写法是以下的简写形式
-record(rec, {field1 :: Type1, field2 :: any(), field3 :: Type3}).
在存在字段的初始值的情况下,必须在初始化之后声明类型,如下所示
-record(rec, {field1 = [] :: Type1, field2, field3 = 42 :: Type3}).
当然,字段的初始值应与相应的类型兼容(即,属于该类型)。编译器会对此进行检查,如果检测到违规,则会导致编译错误。对于没有初始值的字段,单例类型 'undefined'
将添加到所有声明的类型中。换句话说,以下两个记录声明具有相同的效果
-record(rec, {f1 = 42 :: integer(),
f2 :: float(),
f3 :: 'a' | 'b').
-record(rec, {f1 = 42 :: integer(),
f2 :: 'undefined' | float(),
f3 :: 'undefined' | 'a' | 'b').
因此,建议尽可能在记录中包含初始化器。
任何包含类型信息的记录,一旦定义,都可以使用以下语法作为类型
#rec{}
此外,当使用记录类型时,可以通过以下方式添加有关字段的类型信息,来进一步指定记录字段
#rec{some_field :: Type}
任何未指定的字段都假定具有原始记录声明中的类型。
使用新的编译器属性 'spec'
提供函数的合同(或规范)。基本格式如下
-spec Module:Function(ArgType1, ..., ArgTypeN) -> ReturnType.
函数的元数必须与参数的数量匹配,否则会发生编译错误。
此形式也可以在头文件 (.hrl) 中使用,以声明导出函数的类型信息。然后,这些头文件可以包含在(隐式或显式)导入这些函数的文件中。
对于给定模块中的大多数用法,允许使用以下简写形式
-spec Function(ArgType1, ..., ArgTypeN) -> ReturnType.
此外,出于文档目的,可以给出参数名称
-spec Function(ArgName1 :: Type1, ..., ArgNameN :: TypeN) -> RT.
函数规范可以重载。也就是说,它可以有多个类型,用分号 (;) 分隔
-spec foo(T1, T2) -> T3
; (T4, T5) -> T6.
当前的一个限制(目前会导致编译器发出警告(OBS:而不是错误))是参数类型的域不能重叠。例如,以下规范会导致警告
-spec foo(pos_integer()) -> pos_integer()
; (integer()) -> integer().
类型变量可以在规范中用来指定函数的输入和输出参数的关系。例如,以下规范定义了多态恒等函数的类型
-spec id(X) -> X.
但是,请注意,以上规范不以任何方式限制输入和输出类型。我们可以通过类似守卫的子类型约束来约束这些类型
-spec id(X) -> X when is_subtype(X, tuple()).
并提供有界量化。目前,is_subtype/2
守卫是唯一可以在 'spec'
属性中使用的守卫。
is_subtype/2
约束的范围是其后出现的 (...) -> RetType
规范。为避免混淆,我们建议在重载合同的不同组成部分中使用不同的变量,如下例所示
-spec foo({X, integer()}) -> X when is_subtype(X, atom())
; ([Y]) -> Y when is_subtype(Y, number()).
Erlang 中的某些函数并非旨在返回;要么是因为它们定义了服务器,要么是因为它们用于引发异常,如下面的函数所示
my_error(Err) -> erlang:throw({error, Err}).
对于此类函数,我们建议使用特殊的 no_return()
类型作为其“返回”,通过以下形式的合同
-spec my_error(term()) -> no_return().
主要的限制是无法定义递归类型。
本文档已置于公共领域。