檢視原始碼 類型與函數規格
Erlang 類型語言
Erlang 是一種動態類型語言。儘管如此,它仍然提供了一種表示法,用於宣告 Erlang 項的集合以形成特定的類型。這有效地形成了所有 Erlang 項集合的特定子類型。
隨後,這些類型可以用於指定記錄欄位的類型,以及函數的參數和返回類型。
類型資訊可用於以下目的:
本節描述的類型語言預期將取代 EDoc 使用的純粹基於註解的 @type
和 @spec
宣告。
類型及其語法
類型描述 Erlang 項的集合。類型由一組預定義的類型組成和建立,例如 integer/0
、atom/0
和 pid/0
。預定義類型代表屬於此類型的通常無限的 Erlang 項集合。例如,類型 atom/0
表示所有 Erlang 原子的集合。
對於整數和原子,允許使用單例類型;例如,整數 -1
和 42
,或原子 'foo'
和 'bar'
。所有其他類型都是使用預定義類型或單例類型的聯合建立的。在類型與其子類型之一之間的類型聯合中,子類型會被超類型吸收。因此,該聯合會被視為子類型不是該聯合的組成部分。例如,類型聯合
atom() | 'bar' | integer() | 42
描述的項集合與類型聯合相同
atom() | integer()
由於類型之間存在子類型關係,除 dynamic/0
之外的所有類型都形成一個格,其中最頂層的元素 any/0
表示所有 Erlang 項的集合,而最底層的元素 none/0
表示空的項集合。
為了方便 Erlang 的漸進式類型,提供了類型 dynamic/0
。類型 dynamic/0
表示靜態未知的類型。它類似於 Python 中的 Any、TypeScript 中的 any 和 Hack 中的 dynamic。any/0
和 dynamic/0
以相同的方式與 成功類型互動,因此 Dialyzer 不會區分它們。
預定義類型集合和類型語法如下:
Type :: any() %% The top type, the set of all Erlang terms
| none() %% The bottom type, contains no terms
| dynamic()
| 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 an Integer_Value that evaluates to a positive integer
| <<_:_*N>> %% N is an Integer_Value that evaluates to a positive integer
| <<_:M, _:_*N>>
Fun :: fun() %% any function
| fun((...) -> Type) %% any arity, returning Type
| fun(() -> Type)
| fun((TList) -> Type)
Integer :: integer()
| Integer_Value
| Integer_Value..Integer_Value %% specifies an integer range
Integer_Value :: Erlang_Integer %% ..., -1, 0, 1, ... 42 ...
| Erlang_Character %% $a, $b ...
| Integer_Value BinaryOp Integer_Value
| UnaryOp Integer_Value
BinaryOp :: '*' | 'div' | 'rem' | 'band' | '+' | '-' | 'bor' | 'bxor' | 'bsl' | 'bsr'
UnaryOp :: '+' | '-' | 'bnot'
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 :: #{} %% 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>>
和 <<>>
是 M
或 N
或兩者皆為零的情況下的便捷速記法。
由於列表很常用,它們有速記類型表示法。類型 list(T)
和 nonempty_list(T)
的速記分別為 [T]
和 [T,...]
。這兩個速記之間的唯一區別是 [T]
可以是空列表,但 [T,...]
不能。
請注意,list/0
(也就是說,未知類型元素的列表)的速記是 [_]
(或 [any()]
),而不是 []
。符號 []
指定空列表的單例類型。
對應類型的通用形式為 #{AssociationList}
。AssociationList
中的鍵類型允許重疊,如果它們重疊,則最左側的關聯優先。如果對應類型中的鍵屬於此類型,則該對應類型中具有鍵。AssociationList
可以包含強制 (:=)
和可選 (=>)
對應類型。如果對應類型是強制性的,則需要存在具有該類型的對應類型。在可選的對應類型的情況下,不需要存在鍵類型。
符號 #{}
指定空對應的單例類型。請注意,此符號不是 map/0
類型的速記。
為了方便起見,還內建了以下類型。它們可以被視為下表中也顯示的類型聯合的預定義別名。
內建類型 | 定義為 |
---|---|
term/0 | any/0 |
binary/0 | <<_:_*8>> |
nonempty_binary/0 | <<_:8, _:_*8>> |
bitstring/0 | <<_:_*1>> |
nonempty_bitstring/0 | <<_:1, _:_*1>> |
boolean/0 | 'false' | 'true' |
byte/0 | 0..255 |
char/0 | 0..16#10ffff |
nil/0 | [] |
number/0 | integer/0 | float/0 |
list/0 | [any()] |
maybe_improper_list/0 | maybe_improper_list(any(), any()) |
nonempty_list/0 | nonempty_list(any()) |
string/0 | [char()] |
nonempty_string/0 | [char(),...] |
iodata/0 | iolist() | binary() |
iolist/0 | maybe_improper_list(byte() | binary() | iolist(), binary() | []) |
map/0 | #{any() => any()} |
function/0 | fun() |
module/0 | atom/0 |
mfa/0 | {module(),atom(),arity()} |
arity/0 | 0..255 |
identifier/0 | pid() | port() | reference() |
node/0 | atom/0 |
timeout/0 | 'infinity' | non_neg_integer() |
no_return/0 | none/0 |
表格:內建類型,預定義別名
此外,還存在以下三個內建類型,並且可以認為它們的定義如下,儘管嚴格來說,它們的「類型定義」不符合上述類型語言定義的有效語法。
內建類型 | 可以被認為由以下語法定義 |
---|---|
non_neg_integer/0 | 0.. |
pos_integer/0 | 1.. |
neg_integer/0 | ..-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}
記錄會擴展到可能包含類型資訊。這在記錄宣告中的類型資訊中描述。
重新定義內建類型
變更
從 Erlang/OTP 26 開始,允許定義與內建類型同名的類型。
建議避免故意重複使用內建名稱,因為這可能會造成混淆。但是,當 Erlang/OTP 版本引入新類型時,恰好定義了具有相同名稱的自身類型的程式碼將繼續運作。
例如,假設 Erlang/OTP 42 版本引入了一個新的類型 gadget()
,定義如下:
-type gadget() :: {'gadget', reference()}.
進一步假設某些程式碼對 gadget()
有自己的(不同的)定義,例如
-type gadget() :: #{}.
由於允許重新定義,程式碼仍會編譯(但會出現警告),並且 Dialyzer 不會發出任何其他警告。
使用者定義類型的類型宣告
如所見,類型的基本語法是原子後跟封閉的括號。使用 -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
的類型表示一組項,這些項的結構不應從其定義模組的外部可見。也就是說,只有定義它們的模組才允許依賴其項結構。因此,此類類型作為模組本機類型沒有多大意義(模組本機類型無論如何都無法被其他模組存取),並且始終應匯出。
閱讀更多關於不透明類型的資訊
記錄宣告中的類型資訊
記錄欄位的類型可以在記錄宣告中指定。其語法如下:
-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'
,您必須明確地將其加入型別規格中,如第二個範例所示。
任何記錄,無論是否包含型別資訊,一旦定義後,都可以使用以下語法作為型別使用
#rec{}
此外,當使用記錄型別時,可以透過加入有關欄位的型別資訊來進一步指定記錄欄位,如下所示
#rec{some_field :: Type}
任何未指定的欄位都會被假定為具有原始記錄宣告中的型別。
注意
當記錄用於建立 ETS 和 Mnesia 比對函式的模式時,Dialyzer 可能需要一些協助,以避免發出錯誤的警告。例如
-type height() :: pos_integer(). -record(person, {name :: string(), height :: height()}). lookup(Name, Tab) -> ets:match_object(Tab, #person{name = Name, _ = '_'}).
Dialyzer 會發出警告,因為
'_'
不在記錄欄位height
的型別中。處理這個問題的建議方法是宣告最小的記錄欄位型別,以滿足您的所有需求,然後根據需要建立改進。修改後的範例
-record(person, {name :: string(), height :: height() | '_'}). -type person() :: #person{height :: height()}.
在規格和型別宣告中,應該優先使用型別
person()
而不是#person{}
。
函式的規格
函式的規格(或合約)使用 -spec
屬性給定。一般格式如下
-spec Function(ArgType1, ..., ArgTypeN) -> ReturnType.
在目前的模組中必須存在一個名稱相同的函式 Function
的實作,並且函式的參數數量必須與引數數量相符,否則編譯會失敗。
只要 Module
是目前模組的名稱,以下包含模組名稱的較長格式也有效。這對於文件編寫很有用。
-spec Module:Function(ArgType1, ..., ArgTypeN) -> ReturnType.
此外,為了方便文件編寫,可以給定引數名稱
-spec Function(ArgName1 :: Type1, ..., ArgNameN :: TypeN) -> RT.
函式規格可以被多載。也就是說,它可以有幾個型別,用分號 (;
) 分隔。例如
-spec foo(T1, T2) -> T3;
(T4, T5) -> T6.
目前的一個限制,目前會導致 Dialyzer 發出警告,是引數型別的定義域不能重疊。例如,以下規格會導致警告
-spec foo(pos_integer()) -> pos_integer();
(integer()) -> integer().
型別變數可以用在規格中,以指定函式的輸入和輸出引數之間的關係。例如,以下規格定義了多型識別函式的型別
-spec id(X) -> X.
請注意,上述規格並未以任何方式限制輸入和輸出型別。這些型別可以透過類似 guard 的子型別限制來約束,並提供有限的量化
-spec id(X) -> X when X :: tuple().
目前,::
限制(讀作「是子型別」)是唯一可以用在 -spec
屬性的 when
部分的 guard 限制。
注意
上述函式規格使用了相同型別變數的多個出現。這提供了比以下沒有型別變數的函式規格更多的型別資訊
-spec id(tuple()) -> tuple().
後者規格表示函式接受某個 tuple 並返回某個 tuple。具有
X
型別變數的規格指定函式接受一個 tuple 並返回相同的 tuple。但是,是否要考慮這個額外的資訊,取決於處理規格的工具。
::
限制的範圍是其後出現的 (...) -> RetType
規格。為了避免混淆,建議在多載合約的不同組成部分中使用不同的變數,如下例所示
-spec foo({X, integer()}) -> X when X :: atom();
([Y]) -> Y when Y :: number().
Erlang 中的某些函式並非旨在返回;它們可能是用於定義伺服器,或是用於拋出例外,如下面的函式所示
my_error(Err) -> throw({error, Err}).
對於這類函式,建議使用特殊的 no_return/0
型別作為它們的「返回」,透過以下形式的合約
-spec my_error(term()) -> no_return().
注意
Erlang 使用速記版本
_
作為匿名型別變數,等效於term/0
或any/0
。例如,以下函式-spec Function(string(), _) -> string().
等效於
-spec Function(string(), any()) -> string().