7.类型和功能规格 | 7. Types and Function Specifications
7类型和功能规格
7.1 Erlang类型语言
Erlang是一种动态类型语言。尽管如此,它还带有一个用于声明Erlang术语集合以形成特定类型的符号。这有效地形成了所有Erlang术语集的特定子类型。
随后,这些类型可用于指定记录字段的类型以及函数的参数和返回类型。
类型信息可用于以下方面:
- 记录函数接口
- 提供更多有关错误检测工具的信息,如Dialyzer
- 可被文档工具(如edoc)利用,以生成各种形式的程序文档。
预计本节中描述的类型语言将取代和取代EDoc使用的纯粹基于注释@type
和@spec
声明。
7.2种类型及其句法
类型描述了一系列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()
| reference()
| [] %% nil
| Atom
| Bitstring
| float()
| Fun
| Integer
| List
| Map
| Tuple
| Union
| UserDefined %% described in Type Declarations of User-Defined Types
Atom :: atom()
| Erlang_Atom %% 'foo', 'bar', ...
Bitstring :: <<>>
| <<_:M>> %% M is a positive integer
| <<_:_*N>> %% N is a positive integer
| <<_:M, _:_*N>>
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)
| maybe_improper_list(Type1, Type2) %% Type1=contents, Type2=termination
| nonempty_improper_list(Type1, Type2) %% Type1 and Type2 as above
| nonempty_list(Type) %% Proper non-empty list
Map :: map() %% denotes a map of any size
| #{} %% denotes the empty map
| #{AssociationList}
Tuple :: tuple() %% denotes a tuple of any size
| {}
| {TList}
AssociationList :: Association
| Association, AssociationList
Association :: Type := Type %% denotes a mandatory association
| Type => Type %% denotes an optional association
TList :: Type
| Type, TList
Union :: Type1 | Type2
位串的一般形式是<<_:M, _:_*N>>,其中M并N是正整数。它表示一个比特字符串,它是比M + (k*N)特长的(也就是说,一个比特串以M比特开始并且继续每个比特的k分段N,其中k也是一个正整数)。对于或(或两者)为零的情况,符号<<_:_*N>>,<<_:M>>和<<>>是便利的简写。MN
由于列表通常被使用,所以它们具有简写类型符号。类型list(T)
和nonempty_list(T)
有速记[T]
和[T,...]
分别。两个shorthands之间的唯一区别是[T]
可以是一个空列表,但[T,...]
不能。
请注意,对于list()
未知类型元素列表的简写是[_]
(或[any()]
)不是[]
。符号[]
指定空列表的单例类型。
地图类型的一般形式是#{AssociationList}
。关键字类型AssociationList
允许重叠,如果他们这样做,最左边的关联优先。如果地图关联AssociationList
属于此类型,则它具有关键字。AssociationList
可以包含强制关联类型和可选关联类型。如果关联类型是强制性的,则会出现与该类型的关联。在可选关联类型的情况下,不需要键类型存在。
请注意map()is #{any() => any()}(或#{_ => _})的语法表示,而不是#{}。符号#{}指定空映射的单例类型。
为了方便,以下类型也是内置的。它们可以被认为是也在表中显示的类型联合的预定义别名。
Built-in type | Defined as |
---|---|
term() | any() |
binary() | <<_:_*8>> |
bitstring() | <<_:_*1>> |
boolean() | 'false' | 'true' |
byte() | 0..255 |
char() | 0..16#10ffff |
nil() | [] |
number() | integer() | float() |
list() | any() |
maybe_improper_list() | maybe_improper_list(any(), any()) |
nonempty_list() | nonempty_list(any()) |
string() | char() |
nonempty_string() | char(),... |
iodata() | iolist() | binary() |
iolist() | maybe_improper_list(byte() | binary() | iolist(), binary() | []) |
function() | fun() |
module() | atom() |
mfa() | {module(),atom(),arity()} |
arity() | 0..255 |
identifier() | pid() | port() | reference() |
node() | atom() |
timeout() | 'infinity' | non_neg_integer() |
no_return() | none() |
另外,存在以下三种内置类型,并且可以认为如下定义,尽管严格地说它们的“类型定义”根据上面定义的类型语言是无效的语法。
内置类型 | 可以被认为由语法定义 |
---|---|
non_neg_integer() | 0.. |
pos_integer() | 1.. |
neg_integer() | ..-1 |
不允许用户定义与预定义或内置类型名称相同的类型。这由编译器检查,其违规导致编译错误。
注
以下内置列表类型也存在,但它们预计很少使用。因此,他们有很长的名字:
nonempty_maybe_improper_list() :: nonempty_maybe_improper_list(any(), any())
nonempty_improper_list(Type1, Type2)
nonempty_maybe_improper_list(Type1, Type2)
在最后两种类型中,定义了预期的Erlang术语集。
同样为了方便起见,允许使用记录符号。记录是相应元组的简写:
Record :: #Erlang_Atom{}
| #Erlang_Atom{Fields}
记录被扩展到可能包含类型信息。这在描述中Type Information in Record Declarations
。
7.3用户定义类型的类型声明
正如所看到的,一个类型的基本语法是一个原子,后面是闭括号。新类型使用-type
和-opaque
属性声明如下:
-type my_struct_type() :: Type.
-opaque my_opaq_type() :: Type.
类型名称是原子my_struct_type
,然后是括号。Type
是上一节中定义的类型。当前的限制是Type
只能包含预定义的类型或用户定义的类型,它们是以下任一类型:
- 模块本地类型,即模块代码中存在的定义
- 远程类型,即在其他模块中定义和输出的类型; 更多关于这一点。
对于本地模块类型,其定义存在于模块中的限制由编译器强制执行,并导致编译错误。(目前存在类似的限制记录。)
类型声明也可以通过在括号之间包含类型变量来参数化。类型变量的语法与Erlang变量相同,即以大写字母开头。自然,这些变量可以 - 也将会 - 出现在定义的RHS上。具体的例子如下:
-type orddict(Key, Val) :: [{Key, Val}].
模块可以导出某些类型来声明允许其他模块将它们称为远程类型
。该声明具有以下形式:
-export_type([T1/A1, ..., Tk/Ak]).
这里的Ti是原子(类型的名称),Ai是他们的论点
例子:
-export_type([my_struct_type/0, orddict/2]).
假设这些类型是从模块中导出的'mod'
,您可以使用如下的远程类型表达式从其他模块中引用它们:
mod:my_struct_type()
mod:orddict(atom(), term())
不允许引用未声明为导出的类型。
声明为的类型opaque
表示其结构不应该在其定义模块之外可见的术语集。也就是说,只有定义它们的模块才允许依赖于它们的术语结构。因此,这些类型没有多大意义,因为模块本地模块本地类型无论如何都不能被其他模块访问 - 并且始终要导出。
7.4记录声明中的类型信息
记录字段的类型可以在记录的声明中指定。这个语法如下:
-record(rec, {field1 :: Type1, field2, field3 :: Type3}).
对于没有键入注释的字段,其类型默认为any()。也就是说,前面的例子是以下的简写:
-record(rec, {field1 :: Type1, field2 :: any(), field3 :: Type3}).
在存在字段初始值的情况下,必须在初始化后声明类型,如下所示:
-record(rec, {field1 = [] :: Type1, field2, field3 = 42 :: Type3}).
字段的初始值应与相应类型的(即,其成员)兼容。这由编译器检查,如果检测到违规,则会导致编译错误。
注
在Erlang/OTP 19之前,对于没有初始值的字段,单例类型'undefined'
被添加到所有声明的类型中。换句话说,以下两条记录声明具有相同的效果:
-record(rec, {f1 = 42 :: integer(),
f2 :: float(),
f3 :: 'a' | 'b'}).
-record(rec, {f1 = 42 :: integer(),
f2 :: 'undefined' | float(),
f3 :: 'undefined' | 'a' | 'b'}).
这已不再是这种情况。如果您需要'undefined'
使用记录字段类型,则必须将其显式添加到typespec中,如第二个示例中所述。
任何包含或不包含类型信息的记录都可以使用以下语法作为类型使用:
#rec{}
另外,通过添加关于字段的类型信息来使用记录类型时,可以进一步指定记录字段,如下所示:
#rec{some_field :: Type}
任何未指定的字段都假定在原始记录声明中具有该类型。
7.5函数规格
使用-spec
属性给出函数的规范(或契约)。一般格式如下:
-spec Module:Function(ArgType1, ..., ArgTypeN) -> ReturnType.
函数的arity必须与参数的数目相匹配,否则会发生编译错误。
这个表单也可以用在头文件(.hrl)中来声明导出函数的类型信息。然后这些头文件可以被包含在(隐式或显式地)导入这些函数的文件中。
在给定的模块中,在大多数情况下,以下简写符号足够了:
-spec Function(ArgType1, ..., ArgTypeN) -> ReturnType.
此外,为了编写文档,可以给出参数名称:
-spec Function(ArgName1 :: Type1, ..., ArgNameN :: TypeN) -> RT.
功能规格可能会超载。也就是说,它可以有几种类型,用分号(;
)分隔:
-spec foo(T1, T2) -> T3
; (T4, T5) -> T6.
当前的限制,当前会导致编译器发出警告(不是错误),是参数类型的域不能重叠。例如,以下规范会导致警告:
-spec foo(pos_integer()) -> pos_integer()
; (integer()) -> integer().
类型变量可用于规范中,以指定函数的输入和输出参数的关系。例如,以下规范定义了多态身份函数的类型:
-spec id(X) -> X.
请注意,上述规范不以任何方式限制输入和输出类型。这些类型可以通过类似守卫的子类型约束来约束并提供有界的量化:
-spec id(X) -> X when X :: tuple().
目前,::
约束(读为“是...的一个子类型”)是唯一可用于属性when
部分的警戒约束-spec
。
注
上面的函数说明使用了多个相同类型的变量。这提供了比以下函数说明更多的类型信息,其中缺少类型变量:
-spec id(tuple()) -> tuple().
后面的规范说这个函数需要一些元组并返回一些元组。带有X
类型变量的规范指定该函数接受一个元组并返回相同的
元组。
但是,处理规格的工具由选择是否考虑这些额外信息决定。
::约束的范围是(...) -> RetType它出现之后的规范。为避免混淆,建议在超负荷合约的不同成分中使用不同的变量,如以下示例所示:
-spec foo{X, integer()}) -> X when X :: atom()
; ([Y]) -> Y when Y :: number().
Erlang中的一些函数并不意味着返回; 要么是因为它们定义了服务器,要么是因为它们用于抛出异常,如下面的函数所示:
my_error(Err) -> erlang:throw{error, Err}).
对于这样的函数,建议no_return()
通过以下形式的合约将“ 特殊类型”用于其“退货”:
-spec my_error(term()) -> no_return().