檢視原始碼 類型與函數規格

Erlang 類型語言

Erlang 是一種動態類型語言。儘管如此,它仍然提供了一種表示法,用於宣告 Erlang 項的集合以形成特定的類型。這有效地形成了所有 Erlang 項集合的特定子類型。

隨後,這些類型可以用於指定記錄欄位的類型,以及函數的參數和返回類型。

類型資訊可用於以下目的:

  • 記錄函數介面
  • 為錯誤偵測工具(如 Dialyzer)提供更多資訊
  • 供文件工具(如 ExDocEDoc)利用,以產生文件

本節描述的類型語言預期將取代 EDoc 使用的純粹基於註解的 @type@spec 宣告。

類型及其語法

類型描述 Erlang 項的集合。類型由一組預定義的類型組成和建立,例如 integer/0atom/0pid/0。預定義類型代表屬於此類型的通常無限的 Erlang 項集合。例如,類型 atom/0 表示所有 Erlang 原子的集合。

對於整數和原子,允許使用單例類型;例如,整數 -142,或原子 '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 中的 dynamicany/0dynamic/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>>,其中 MN 必須計算為正整數。它表示一個長度為 M + (k*N) 位元的位元字串(也就是說,一個以 M 位元開始並繼續包含 kN 位元片段的位元字串,其中 k 也是一個正整數)。符號 <<_:_*N>><<_:M>><<>>MN 或兩者皆為零的情況下的便捷速記法。

由於列表很常用,它們有速記類型表示法。類型 list(T)nonempty_list(T) 的速記分別為 [T][T,...]。這兩個速記之間的唯一區別是 [T] 可以是空列表,但 [T,...] 不能。

請注意,list/0(也就是說,未知類型元素的列表)的速記是 [_](或 [any()]),而不是 []。符號 [] 指定空列表的單例類型。

對應類型的通用形式為 #{AssociationList}AssociationList 中的鍵類型允許重疊,如果它們重疊,則最左側的關聯優先。如果對應類型中的鍵屬於此類型,則該對應類型中具有鍵。AssociationList 可以包含強制 (:=) 和可選 (=>) 對應類型。如果對應類型是強制性的,則需要存在具有該類型的對應類型。在可選的對應類型的情況下,不需要存在鍵類型。

符號 #{} 指定空對應的單例類型。請注意,此符號不是 map/0 類型的速記。

為了方便起見,還內建了以下類型。它們可以被視為下表中也顯示的類型聯合的預定義別名。

內建類型定義為
term/0any/0
binary/0<<_:_*8>>
nonempty_binary/0<<_:8, _:_*8>>
bitstring/0<<_:_*1>>
nonempty_bitstring/0<<_:1, _:_*1>>
boolean/0'false' | 'true'
byte/00..255
char/00..16#10ffff
nil/0[]
number/0integer/0 | float/0
list/0[any()]
maybe_improper_list/0maybe_improper_list(any(), any())
nonempty_list/0nonempty_list(any())
string/0[char()]
nonempty_string/0[char(),...]
iodata/0iolist() | binary()
iolist/0maybe_improper_list(byte() | binary() | iolist(), binary() | [])
map/0#{any() => any()}
function/0fun()
module/0atom/0
mfa/0{module(),atom(),arity()}
arity/00..255
identifier/0pid() | port() | reference()
node/0atom/0
timeout/0'infinity' | non_neg_integer()
no_return/0none/0

表格:內建類型,預定義別名

此外,還存在以下三個內建類型,並且可以認為它們的定義如下,儘管嚴格來說,它們的「類型定義」不符合上述類型語言定義的有效語法。

內建類型可以被認為由以下語法定義
non_neg_integer/00..
pos_integer/01..
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/0any/0。例如,以下函式

-spec Function(string(), _) -> string().

等效於

-spec Function(string(), any()) -> string().