檢視原始碼 類型規格參考
Elixir 附帶一個用於宣告類型和規格的符號。本文件是對其用途和語法的參考。
Elixir 是一種動態類型語言,因此,類型規格從未被編譯器用於最佳化或修改程式碼。不過,使用類型規格很有用,因為
類型規格(最常稱為類型規格)是在不同環境中使用下列屬性定義的
@type
@opaque
@typep
@spec
@callback
@macrocallback
此外,你可以使用 @typedoc
來記錄自訂 @type
定義。
請參閱下列「使用者定義類型」和「定義規格」子部分,以取得有關定義類型和類型規格的更多資訊。
一個簡單的範例
defmodule StringHelpers do
@typedoc "A word from the dictionary"
@type word() :: String.t()
@spec long_word?(word()) :: boolean()
def long_word?(word) when is_binary(word) do
String.length(word) > 8
end
end
在上面的範例中
我們宣告一個新類型 (
word()
),它等於字串類型 (String.t()
)。我們使用
@typedoc
來描述類型,它會包含在產生的文件中。我們指定
long_word?/1
函式接收類型為word()
的參數,並傳回布林值 (boolean()
),也就是true
或false
。
類型及其語法
Elixir 提供的類型規格語法類似於 Erlang 中的語法。Erlang 中提供的大部分內建類型(例如 pid()
)都以相同的方式表示:pid()
(或僅為 pid
)。參數化類型(例如 list(integer)
)也受支援,遠端類型(例如 Enum.t()
)也是如此。整數和原子字面值可作為類型(例如 1
、:atom
或 false
)。所有其他類型都建立在預定義類型的聯集之上。允許使用一些簡寫,例如 [...]
、<<>>
和 {...}
。
表示類型聯集的符號為管線符號 |
。例如,類型規格 type :: atom() | pid() | tuple()
會建立一個類型 type
,它可以是 atom
、pid
或 tuple
。這通常在其他語言中稱為 標記聯集
基本類型
type ::
any() # the top type, the set of all terms
| none() # the bottom type, contains no terms
| atom()
| map() # any map
| pid() # process identifier
| port() # port identifier
| reference()
| tuple() # tuple of any size
## Numbers
| float()
| integer()
| neg_integer() # ..., -3, -2, -1
| non_neg_integer() # 0, 1, 2, 3, ...
| pos_integer() # 1, 2, 3, ...
## Lists
| list(type) # proper list ([]-terminated)
| nonempty_list(type) # non-empty proper list
| maybe_improper_list(content_type, termination_type) # proper or improper list
| nonempty_improper_list(content_type, termination_type) # improper list
| nonempty_maybe_improper_list(content_type, termination_type) # non-empty proper or improper list
| Literals # Described in section "Literals"
| BuiltIn # Described in section "Built-in types"
| Remotes # Described in section "Remote types"
| UserDefined # Described in section "User-defined types"
字面值
類型規格中也支援下列字面值
type :: ## Atoms
:atom # atoms: :foo, :bar, ...
| true | false | nil # special atom literals
## Bitstrings
| <<>> # empty bitstring
| <<_::size>> # size is 0 or a positive integer
| <<_::_*unit>> # unit is an integer from 1 to 256
| <<_::size, _::_*unit>>
## (Anonymous) Functions
| (-> type) # zero-arity, returns type
| (type1, type2 -> type) # two-arity, returns type
| (... -> type) # any arity, returns type
## Integers
| 1 # integer
| 1..10 # integer from 1 to 10
## Lists
| [type] # list with any number of type elements
| [] # empty list
| [...] # shorthand for nonempty_list(any())
| [type, ...] # shorthand for nonempty_list(type)
| [key: value_type] # keyword list with optional key :key of value_type
## Maps
| %{} # empty map
| %{key: value_type} # map with required key :key of value_type
| %{key_type => value_type} # map with required pairs of key_type and value_type
| %{required(key_type) => value_type} # map with required pairs of key_type and value_type
| %{optional(key_type) => value_type} # map with optional pairs of key_type and value_type
| %SomeStruct{} # struct with all fields of any type
| %SomeStruct{key: value_type} # struct with required key :key of value_type
## Tuples
| {} # empty tuple
| {:ok, type} # two-element tuple with an atom and any type
內建類型
Elixir 也提供下列類型,作為上述基本類型和字面值類型的捷徑。
內建類型 | 定義為 |
---|---|
term() | any() |
arity() | 0..255 |
as_boolean(t) | t |
binary() | <<_::_*8>> |
nonempty_binary() | <<_::8, _::_*8>> |
bitstring() | <<_::_*1>> |
nonempty_bitstring() | <<_::1, _::_*1>> |
boolean() | true | false |
byte() | 0..255 |
char() | 0..0x10FFFF |
charlist() | [char()] |
nonempty_charlist() | [char(), ...] |
fun() | (... -> any) |
function() | fun() |
identifier() | pid() | port() | reference() |
iodata() | iolist() | binary() |
iolist() | maybe_improper_list(byte() | binary() | iolist(), binary() | []) |
keyword() | [{atom(), any()}] |
keyword(t) | [{atom(), t}] |
list() | [任何()] |
非空清單() | 非空清單(任何()) |
可能不當清單() | 可能不當清單(任何(), 任何()) |
非空可能不當清單() | 非空可能不當清單(任何(), 任何()) |
mfa() | {模組(), 原子(), 項數()} |
模組() | 原子() |
無回傳() | 無() |
節點() | 原子() |
數字() | 整數() | 浮點數() |
結構() | %{:__struct__ => 原子(), optional(原子()) => 任何()} |
逾時() | :無限 | 非負整數() |
as_boolean(t)
存在,用於向使用者表示給定的值將被視為布林值,其中 nil
和 false
將評估為 false
,其他所有值都為 true
。例如,Enum.filter/2
具有以下規格:filter(t, (element -> as_boolean(term))) :: 清單
。
遠端類型
任何模組也能定義自己的類型,而 Elixir 中的模組也不例外。例如,範圍
模組定義一個 t/0
類型,用於表示範圍:此類型可以稱為 範圍.t/0
。類似地,字串是 字串.t/0
,依此類推。
對應
對應中的鍵類型允許重疊,如果重疊,最左邊的鍵優先。如果對應值包含不在允許對應鍵中的鍵,則該值不屬於此類型。
如果你想要表示先前未定義在對應中的鍵是允許的,通常會以 optional(any) => any
結束對應類型。
請注意,map()
的語法表示為 %{optional(any) => any}
,而不是 %{}
。表示法 %{}
指定空地圖的單例類型。
關鍵字清單
除了 keyword()
和 keyword(t)
之外,撰寫預期關鍵字清單的規範也很有幫助。例如
@type option :: {:name, String.t} | {:max, pos_integer} | {:min, pos_integer}
@type options :: [option()]
這清楚地表明只允許這些選項,沒有任何選項是必需的,而且順序並不重要。
它還允許與現有類型組合。例如
type option :: {:my_option, String.t()} | GenServer.option()
@spec start_link([option()]) :: GenServer.on_start()
def start_link(opts) do
{my_opts, gen_server_opts} = Keyword.split(opts, [:my_option])
GenServer.start_link(__MODULE__, my_opts, gen_server_opts)
end
使用者定義的類型
可以使用 @type
、@typep
和 @opaque
模組屬性來定義新的類型
@type type_name :: type
@typep type_name :: type
@opaque type_name :: type
使用 @typep
定義的類型是私有的。使用 @opaque
定義的不透明類型是一種類型,其中類型的內部結構將不可見,但類型仍然是公開的。
類型可以透過將變數定義為參數來參數化;然後可以使用這些變數來定義類型。
@type dict(key, value) :: [{key, value}]
定義規範
函數的規範可以定義如下
@spec function_name(type1, type2) :: return_type
守衛可用於限制傳遞給函數的類型變數。
@spec function(arg) :: [arg] when arg: atom
如果您想指定多個變數,請用逗號分隔它們。
@spec function(arg1, arg2) :: {arg1, arg2} when arg1: atom, arg2: integer
沒有限制的類型變數也可以使用 var
定義。
@spec function(arg) :: [arg] when arg: var
此守衛表示法僅適用於 @spec
、@callback
和 @macrocallback
。
您還可以在類型規範中使用 arg_name :: arg_type
語法命名您的引數。這在文件編寫中特別有用,可以用來區分同類型多個引數(或類型定義中同類型的多個元素)
@spec days_since_epoch(year :: integer, month :: integer, day :: integer) :: integer
@type color :: {red :: integer, green :: integer, blue :: integer}
規範可以像一般函數一樣重載。
@spec function(integer) :: atom
@spec function(atom) :: integer
行為
Elixir(和 Erlang)中的行為是一種將元件的通用部分(成為行為模組)與特定部分(成為回呼模組)分開和抽象出來的方法。
行為模組定義了一組函數和巨集(稱為回呼),實作該行為的回呼模組必須匯出這些函數和巨集。此「介面」識別元件的特定部分。例如,GenServer
行為和函數將「伺服器」處理序可能想要從特定部分(例如此伺服器處理序必須執行的動作)實作的所有訊息傳遞(傳送和接收)和錯誤報告抽象出來。
假設我們想要實作一堆解析器,每個解析器都解析結構化資料:例如,JSON 解析器和 MessagePack 解析器。這兩個解析器每個都會表現相同的方式:兩者都會提供一個 parse/1
函式和一個 extensions/0
函式。parse/1
函式會傳回結構化資料的 Elixir 表示,而 extensions/0
函式會傳回一個檔案副檔名清單,可用於每種資料類型(例如,JSON 檔案的 .json
)。
我們可以建立一個 Parser
行為
defmodule Parser do
@doc """
Parses a string.
"""
@callback parse(String.t) :: {:ok, term} | {:error, atom}
@doc """
Lists all supported file extensions.
"""
@callback extensions() :: [String.t]
end
如上方的範例所示,定義一個 callback 是定義一個 callback 規格的問題,由下列組成
- callback 名稱(範例中的
parse
或extensions
) - callback 必須接受的引數(
String.t
) - callback 回傳值的預期類型
採用 Parser
行為的模組必須實作所有使用 @callback
屬性定義的函式。如你所見,@callback
預期一個函式名稱,但也會預期一個函式規格,就像我們在上方看到的 @spec
屬性所使用的規格。
實作行為
實作一個行為很簡單
defmodule JSONParser do
@behaviour Parser
@impl Parser
def parse(str), do: {:ok, "some json " <> str} # ... parse JSON
@impl Parser
def extensions, do: [".json"]
end
defmodule CSVParser do
@behaviour Parser
@impl Parser
def parse(str), do: {:ok, "some csv " <> str} # ... parse CSV
@impl Parser
def extensions, do: [".csv"]
end
如果採用特定行為的模組沒有實作該行為所需的其中一個 callback,就會產生一個編譯時間警告。
此外,使用 @impl
你也可以確保你明確地實作了特定行為的正確 callback。例如,下列解析器實作了 parse
和 extensions
。然而,由於一個錯字,BADParser
實作了 parse/0
,而不是 parse/1
。
defmodule BADParser do
@behaviour Parser
@impl Parser
def parse, do: {:ok, "something bad"}
@impl Parser
def extensions, do: ["bad"]
end
這段程式碼會產生一個警告,讓你得知你錯誤地實作了 parse/0
,而不是 parse/1
。你可以在 模組文件 中進一步了解 @impl
。
使用行為
行為很有用,因為你可以傳遞模組作為引數,然後你可以呼叫回行為中指定的任何函式。例如,我們可以有一個函式,接收一個檔名、幾個解析器,並根據其副檔名解析檔案
@spec parse_path(Path.t(), [module()]) :: {:ok, term} | {:error, atom}
def parse_path(filename, parsers) do
with {:ok, ext} <- parse_extension(filename),
{:ok, parser} <- find_parser(ext, parsers),
{:ok, contents} <- File.read(filename) do
parser.parse(contents)
end
end
defp parse_extension(filename) do
if ext = Path.extname(filename) do
{:ok, ext}
else
{:error, :no_extension}
end
end
defp find_parser(ext, parsers) do
if parser = Enum.find(parsers, fn parser -> ext in parser.extensions() end) do
{:ok, parser}
else
{:error, :no_matching_parser}
end
end
你也可以直接呼叫任何解析器:CSVParser.parse(...)
。
請注意,你不需要定義一個行為來動態派送模組,但這些功能通常會齊頭並進。
選擇性 callback
選擇性 callback 是 callback 模組可以實作的 callback,但不是必要的。通常,行為模組會根據設定檔得知是否應該呼叫這些 callback,或者檢查 callback 是否使用 function_exported?/3
或 macro_exported?/3
匯出。
選擇性 callback 可以透過 @optional_callbacks
模組屬性定義,它必須是一個關鍵字清單,其中函式或巨集名稱為鍵,而元數為值。例如
defmodule MyBehaviour do
@callback vital_fun() :: any
@callback non_vital_fun() :: any
@macrocallback non_vital_macro(arg :: any) :: Macro.t
@optional_callbacks non_vital_fun: 0, non_vital_macro: 1
end
Elixir 標準函式庫中可選回呼的一個範例是 GenServer.format_status/2
。
檢查行為
屬性 @callback
和 @optional_callbacks
用於建立一個 behaviour_info/1
函式,可於定義的模組中使用。這個函式可用於擷取由該模組定義的回呼和可選回呼。
例如,對於上面「可選回呼」中定義的 MyBehaviour
模組
MyBehaviour.behaviour_info(:callbacks)
#=> [vital_fun: 0, "MACRO-non_vital_macro": 2, non_vital_fun: 0]
MyBehaviour.behaviour_info(:optional_callbacks)
#=> ["MACRO-non_vital_macro": 2, non_vital_fun: 0]
使用 iex
時,輔助函式 IEx.Helpers.b/1
也可用。
陷阱
使用類型規格時有一些已知的陷阱,如下所述。
類型 string()
Elixir 不鼓勵使用類型 string()
。類型 string()
指的是 Erlang 字串,在 Elixir 中稱為「字元清單」。它們並非指 Elixir 字串,後者是 UTF-8 編碼的二進位資料。為避免混淆,如果您嘗試使用類型 string()
,Elixir 會發出警告。您應該適當地使用 charlist()
、nonempty_charlist()
、binary()
或 String.t()
,或任何這些類型的文字表示。
請注意,對於分析工具而言,String.t()
和 binary()
是等效的。不過,對於閱讀文件的人來說,String.t()
表示它是一個 UTF-8 編碼的二進位資料。
會引發錯誤的函式
類型規格不需要指出函式可能會引發錯誤;如果輸入無效,任何函式都可能隨時失敗。過去,Elixir 標準函式庫有時會使用 no_return()
來表示這一點,但這些用法已移除。
類型 no_return()
也不應使用於會傳回值但其目的是「副作用」的函式,例如 IO.puts/1
。在這些情況下,預期的傳回類型是 :ok
。
相反地,no_return()
應用作傳回類型,表示函式永遠無法傳回值。這包括永遠呼叫 receive
的迴圈函式,或專門用於引發錯誤或關閉 VM 的函式。