Erlang類型及函數聲明規格

Erlang類型及函數聲明規格

Author: litaocheng
Mail: [email protected]
Date: 2009.6.8
Copyright: This document has been placed in the public domain.

概述

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()

類型定義不可重名,編譯器可以進行檢測。

注意 : 還存在一些其他 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(Key, Val) :: [{Key, Val}].

在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定義時指明的類型相同。

函數規範定義

函數規範可以通過新引入的關鍵字 'spec' 來定義(摒棄了舊的 @spec 聲明)。 其語法如下:

-spec Module:Function(ArgType1, ..., ArgTypeN) -> ReturnType.

函數的參數數目必須與函數規範定義相同,否則編譯出錯。

在同一個module內部,可以簡化爲:

-spec Function(ArgType1, ..., ArgTypeN) -> ReturnType.

同時,爲了便於我們生成文檔,我們可以指明參數的名稱:

-spec Function(ArgName1 :: Type1, ..., ArgNameN :: TypeN) -> RT.

函數的spec聲明可以重載。通過 ';' 來實現:

-spec foo(pos_integer()) -> pos_integer()
           ; (integer()) -> integer().

我們可以通過spec指明函數的輸入和輸出的某些關係:

-spec id(X) -> X.

但是,對於上面的spec,其對輸入輸出沒有任何限定。我們可以對返回值增加一些類似guard的限定:

-spec id(X) -> X when is_subtype(X, tuple()).

其表示X爲一個tuple類型。目前僅僅支持 is_subtype 是唯一支持的guard。

某些情況下,有些函數是server的主循環,或者忽略返回值,僅僅拋出某個異常,我們可以使用 no_return() 作爲返回值類型:

-spec my_error(term()) -> no_return().
my_error(Err) -> erlang:throw({error, Err}).

使用dialyzer進行靜態分析

我們定義了type及spec,我們可以使用 dialyzer 對代碼進行靜態分析,在運行之前發現 很多低級或者隱藏的錯誤。

生成plt

爲了分析我們的app或者module,我們可以生成一個plt文件(Persistent Lookup Table), 其目的是爲了加速我們的代碼分析過程,plt內部很多類型及函數信息。

首先我們生成一個常用的plt文件, 其包含了以下lib:erts, kernel, stdlib, mnesia, crypto, sasl, ERL_TOP爲erlang的安裝目錄,各個lib因爲erlang版本不同會有所差別,我當前使用R13B(erl 5.7.1):

dialyzer --build_plt -r $ERL_TOP/lib/erts-5.7.1/ebin \
           $ERL_TOP/lib/kernel-2.13.1/ebin \
           $ERL_TOP/lib/stdlib-1.16.1/ebin \
           $ERL_TOP/lib/mnesia-4.4.9/ebin \
           $ERL_TOP/lib/crypto-1.6/ebin \
           $ERL_TOP/lib/sasl-2.1.6/ebin

經過十幾分鐘的的等待,生成了一個~/.dialyzer_plt文件,在生成plt時,可以通過--output_plt 指定生成的plt的名稱。

我們也可以隨時通過: dialyzer --add_to_plt --plt ~/.dialyzer_plt -c path_to_app 添加應用到既有plt中, 也可以通過: dialyzer --remove_from_plt --plt ~/.dialyzer_plt -c path_to_app 從已有plt中刪除某個應用。

例子:

% 生成plt
dialyzer --build_plt -r /usr/local/lib/erlang/lib/erts-5.7.1/ebin \
           /usr/local/lib/erlang/lib/kernel-2.13.1/ebin \
           /usr/local/lib/erlang/lib/stdlib-1.16.1/ebin \
           /usr/local/lib/erlang/lib/mnesia-4.4.9/ebin \
           /usr/local/lib/erlang/lib/crypto-1.6/ebin \
           /usr/local/lib/erlang/lib/sasl-2.1.6/ebin

% 從plt中去處crypto應用
dialyzer --remove_from_plt --plt ~/.dialyzer_plt -c /usr/local/lib/erlang/lib/crypto-1.6/ebin

% 向plt中添加crypto應用
dialyzer --add_to_plt --plt ~/.dialyzer_plt -c /usr/local/lib/erlang/lib/crypto-1.6/ebin

使用dialyzer分析

生成plt後,就可以對我們書寫的應用進行靜態檢查了。

假設我們書寫一個簡單的module(spec/spec.erl):

-module(spec).
-compile([export_all]).
-vsn('0.1').

-spec index(any(), pos_integer(), [any()]) -> non_neg_integer().
index(Key, N, TupleList) ->
   index4(Key, N, TupleList, 0).

index4(_Key, _N, [], _Index) -> 0;
index4(Key, N, [H | _R], Index) when element(N, H) =:= Key -> Index;
index4(Key, N, [_H | R], Index) -> index4(Key, N, R, Index + 1).

% correct:
%-spec fa( non_neg_integer() ) -> pos_integer().
% invalid:
-spec fa( N :: atom() ) -> pos_integer().
fa(0) -> 1;
fa(1) -> 1;
fa(N) -> fa(N-1) + fa(N-2).

-spec some_fun() -> any().
some_fun() ->
   L = [{bar, 23}, {foo, 33}],
   lists:keydelete(1, foo, L).

編譯spec.erl:

erlc +debug_info spec.erl

使用dialyzer進行分析:

dialyzer -r ./spec

顯示結果:

Checking whether the PLT /home/litao/.dialyzer_plt is up-to-date... yes
Proceeding with analysis...
spec.erl:15: Invalid type specification for function 'spec':fa/1. The success typing is (non_neg_integer()) -> pos_integer()
spec.erl:22: Function some_fun/0 has no local return
spec.erl:24: The call lists:keydelete(1,'foo',L::[{'bar',23} | {'foo',33},...]) will never return since it differs in argument position 2 from the success typing arguments: (any(),pos_integer(),maybe_improper_list())
done in 0m0.29s
done (warnings were emitted)

我們可以看到,我們的fa/1函數的spec信息錯誤,我們進行修正:

由
-spec fa( non_neg_integer() ) -> pos_integer().
改爲:
-spec fa( N :: atom() ) -> pos_integer().

some_fun中,lists:keydelete/3參數順序進行修改:

lists:keydelete(1, foo, L).
改爲:
lists:keydelete(foo,1, L).

重新編譯,進行dialyzer分析,提示成功:

litao@litao:~/erltest$ dialyzer -r ./spec
Checking whether the PLT /home/litao/.dialyzer_plt is up-to-date... yes
Proceeding with analysis... done in 0m0.28s
done (passed successfully)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章