檢視原始碼 類型規格參考

Elixir 附帶一個用於宣告類型和規格的符號。本文件是對其用途和語法的參考。

Elixir 是一種動態類型語言,因此,類型規格從未被編譯器用於最佳化或修改程式碼。不過,使用類型規格很有用,因為

  • 它們提供文件(例如,ExDoc 等工具會在文件中顯示類型規格)
  • 它們會被 Dialyzer 等工具使用,這些工具可以分析具有類型規格的程式碼,以找出類型不一致和可能的錯誤

類型規格(最常稱為類型規格)是在不同環境中使用下列屬性定義的

  • @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()),也就是 truefalse

類型及其語法

Elixir 提供的類型規格語法類似於 Erlang 中的語法。Erlang 中提供的大部分內建類型(例如 pid())都以相同的方式表示:pid()(或僅為 pid)。參數化類型(例如 list(integer))也受支援,遠端類型(例如 Enum.t())也是如此。整數和原子字面值可作為類型(例如 1:atomfalse)。所有其他類型都建立在預定義類型的聯集之上。允許使用一些簡寫,例如 [...]<<>>{...}

表示類型聯集的符號為管線符號 |。例如,類型規格 type :: atom() | pid() | tuple() 會建立一個類型 type,它可以是 atompidtuple。這通常在其他語言中稱為 標記聯集

基本類型

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) 存在,用於向使用者表示給定的值將被視為布林值,其中 nilfalse 將評估為 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 名稱(範例中的 parseextensions
  • 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。例如,下列解析器實作了 parseextensions。然而,由於一個錯字,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?/3macro_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 的函式。