作者
Tobias Lindahl <tobias(dot)lindahl(at)it(dot)uu(dot)se> , Kostis Sagonas <kostis(at)it(dot)uu(dot)se>
狀態
最終版/R13B03 提案已記錄並在 OTP R13B03 中實作
類型
標準追蹤
建立日期
2007年12月2日
Erlang 版本
OTP_R12B

EEP 8:類型和函式規格 #

摘要 #

此 EEP 描述 Erlang 語言的擴展,用於宣告一組 Erlang 術語以形成特定類型,有效地形成所有 Erlang 術語集合的特定子類型。隨後,這些類型可以用來指定記錄欄位的類型以及函式的引數和傳回值。

基本原理 #

類型資訊可用於記錄函式介面,為錯誤偵測工具(如 Dialyzer)提供更多資訊,並可由 Edoc 等文件工具利用,以產生各種形式的程式文件。預計本文件中描述的類型語言將取代並最終取代 Edoc 使用的純粹基於註解的 @type 和 @spec 宣告。

規格 #

類型及其語法 #

類型描述 Erlang 術語的集合。類型由一組預定義類型(例如 integer()atom()pid() 等)組成並建立,如下所述。預定義類型代表屬於此類型的通常無限的 Erlang 術語集合。例如,類型 atom() 代表所有 Erlang 原子 的集合。

對於整數和原子,我們允許單例類型(例如,整數 -142 或原子 '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')後面跟著括號。類型是上一節中定義的類型。目前的限制是 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.

函式的 arity 必須與引數的數量匹配,否則會發生編譯錯誤。

此形式也可以用於標頭檔 (.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.

但是,請注意,上述規格並未以任何方式限制輸入和輸出類型。我們可以透過類似 guard 的子類型約束來約束這些類型

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

並提供有界的量化。目前,is_subtype/2 guard 是唯一可以在 'spec' 屬性中使用的 guard。

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

目前的限制 #

主要限制是無法定義遞迴類型。

版權 #

本文檔已置於公有領域。