erlang的类型系统一

时间:2022-06-01 18:46:46

“Erlang是动态类型的语言,因而不能进行静态分析,所生成的文档也不包含有助于理解的类型信息”——这是惯常的看法,广为流行,而且被看作是Erlang 在开发大型系统时的一个短板(大型系统意味着更强烈的静态分析需求和更严重的依赖文档进行沟通)。

然而 Erlang 是一个有着 20多年历史的成熟系统,它早已发展出了一套自己的类型标注系统,不仅用来生成文档,更重要的是可以据此对源码进行静态分析,通过程序来排除一些低级的和隐藏的错误。在这方面,Erlang OTP 的源码本身及其文档就是最好的例子。在 《Erlang程序设计》的附录A部分,对于这个系统的使用已经进行了充分的说明。

需要强调的一点是在 Erlang 语言的背后还有一个活跃的社区(后者更为重要),其 EPP过程一直都在持续不断地推进语言本身的进化。这方面最新的成果便是:在 R13 中,将此前文档级的 @spec,@type标注升级为语言级的 -spec,-type 标注。可以预期的一点是,在未来的版本中,这些方面仍将持续推进。

litaocheng同学的这篇“Erlang类型及函数声明规格”,风格严谨,论述详尽,涵盖了最新的语言特性,是任何一个程序员想要开发“严肃 Erlang程序”的必读文档。

 

Erlang类型及函数声明规格

Author: litaocheng
Mail: litaocheng@gmail.com
Date: 2009.6.8
Copyright: This document has been placed in the publicdomain.
Contents:

  • 概述
  • 意义
  • 规范
    • 类型及其定义语法
    • 自定义类型定义
    • 在record中使用类型声明
    • 函数规范定义
  • 使用dialyzer进行静态分析
    • 生成plt
    • 使用dialyzer分析
  • 参考

概述

Erlang为动态语言,变量在运行时动态绑定,这对于我们获取函数的参数及返回值的类型信息具有一定的难度。为了弥补这个不足,在Erlang中我们可以通过type及spec定义数据类型及函数原型。通过这些信息,我们对函数及调用进行静态检测,从而发现一些代码中问题。同时,这些信息也便于他人了解函数接口,也可以用来生成文档。

意义

  • 定义各种自定义数据类型
  • 定义函数的参数及返回值
  • dialyzer 进行代码静态分析
  • edoc利用这些信息生成文档

规范

类型及其定义语法

数据类型由一系列Erlang terms组成,其有各种基本数据类型组成(如 integer() , atom() , pid())。Erlang预定义数据类型代表属于此类型的所有数据,比如 atom() 代表所有的atom类型的数据。

数据类型,由基本数据类型及其他自定义数据类型组成,其范围为对应数据类型的合集。比如:

atom ()   ' bar '   integer ()   42

与:

atom ()   integer ()

具有相同的含义。

各种类型之间具有一定的层级关系,其中最顶层的 any() 可以代表任何Erlang类型,而最底层的 none()表示空的数据类型。

预定义的类型及语法如下:

Type  ::  any ()           %%最顶层类型,表示任意的Erlang term
    
none ()           %%最底层类型,不包含任何term
    
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 ()                          %%任意函数
   
fun (( ... )  ->  Type )                %%任意arity, 只定义返回类型
   
fun (()  ->  Type )
   
fun (( TList )  ->  Type )
 
Integer  ::  integer ()
      
Erlang_Integer                %%..., -1, 0, 1, ... 42 ...
      
Erlang_Integer .. Erlang_Integer   %%定义一个整数区间
 
List  ::  list ( Type )                     %%格式规范的list (以[]结尾)
    
improper_list ( Type1 Type2 )       %%Type1=contents, Type2=termination
    
maybe_improper_list ( Type1 Type2 )   %%Type1 and Type2 as above
 
Tuple  ::  tuple ()                       %%表示包含任意元素的tuple
     | {}
     | {
TList }
 
TList  ::  Type
     
Type TList

由于 lists 经常使用,我们可以将 list(T) 简写为 [T] ,而 [T, ...]表示一个非空的元素类型为T的规范列表。两者的区别是 [T] 可能为空,而 [T, ...] 至少包含一个元素。

'_' 可以用来表示任意类型。

请注意, list()表示任意类型的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()

类型定义不可重名,编译器可以进行检测。(转载注:在R13,如果采用 -type 和 -spec标注,编译阶段会进行这种检测,然而,因为标注仍然是可选的,所以,如果没有使用标注,则不会进行检测。)

注意 : 还存在一些其他 lists 相关的内建类型,但是因为其名字较长,我们很少使用:

nonempty_maybe_improper_list ( Type )  ::  nonempty_maybe_improper_list ( Type , any ())
nonempty_maybe_improper_list ()  ::  nonempty_maybe_improper_list ( any ())

我们也可以使用record标记法来表示数据类型:

Record  ::# Erlang_Atom {}
       | #
Erlang_Atom { Fields }

当前R13B中,已经支持record定义中的类型说明

 

 

自定义类型定义

通过前一章节的介绍,我们知道基本的类型语法为一个atom紧随一对圆括号。如果我们想第一个一个新类型,需要使用 'type'关键字:

-type my_type() :: Type.

my_type为我们自定义的type名称,其必须为atom,Type为先前章节介绍的各种类型,其可以为内建类型定义,也可以为可见的(已经定义的)自定义数据类型。否则会编译时保错。

这样递归的类型定义,当前还不支持。

类型定义也可以参数化,我们可以在括号中包含类型,如同Erlang中变量定义,这个参数必须以大写字母开头,一个简单的例子:

-type orddict(KeyVal) :: [{KeyVal}].

在record中使用类型声明

我们可以指定record中字段的类型,语法如下:

- record ( rec ,{ field1  ::  Type1 field2 field3  ::  Type3 } ) .

如果字段没有指明类型声明,那么默认为 any() . 比如,上面的record定义与此相同:

- record ( rec ,{ field1  ::  Type1 field2  ::  any () field3  ::  Type3 } ) .

如果我们在定义record的时候,指明了初始值,类型声明必须位于初始值之后:

- record ( rec ,{ field1   []  ::  Type1 field2 field3   42  ::  Type3 } ) $

我们可以指定record中字段的类型,语法如下::

- record ( rec ,{ field1  ::  Type1 field2 field3  ::  Type3 } ) .

如果字段没有指明类型声明,那么默认为 any() . 比如,上面的record定义与此相同:

- record ( rec ,{ field1  ::  Type1 field2  ::  any () field3  ::  Type3 } ) .

如果我们在定义record的时候,指明了初始值,类型声明必须位于初始值之后:

- record ( rec ,{ field1   []  ::  Type1 field2 field3   42  ::  Type3 } ) .

如果初始值类型与字段的类型声明不一致,会产生一个编译期错误。 filed的默认值为 'undefined',因此下面的来个record定义效果相同:

- record ( rec ,{ f1   42  ::  integer () ,
             
f2     ::  float () ,
             
f3     ::  ' a '   ' b ' ) .
 
-
record ( rec ,{ f1   42  ::  integer () ,
             
f2     ::  ' undefined '   float () ,
             
f3     ::  ' undefined '   ' a '   ' b ' ) .

所以,推荐您在定义record时,指明初始值。

record定义后,我们可以作为一个类型来使用,其用法如下:

# rec {}

在使用recored类型时,我们也可以重新指定某个field的类型:

# rec { some_field  ::  Type }

没有指明的filed,类型与record定义时指明的类型相同。