此 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'
)後面跟著括號。類型是上一節中定義的類型。目前的限制是 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().
主要限制是無法定義遞迴類型。
本文檔已置於公有領域。