檢視原始碼 協定 (Elixir v1.16.2)
使用協定的參考和函式。
協定指定其實作應定義的 API。協定使用 Kernel.defprotocol/2
定義,其實作則使用 Kernel.defimpl/3
定義。
實際案例
在 Elixir 中,我們有兩個名詞用於檢查資料結構中有多少個項目:length
和 size
。length
表示必須計算資訊。例如,length(list)
需要遍歷整個清單才能計算其長度。另一方面,tuple_size(tuple)
和 byte_size(binary)
不依賴於元組和二進位大小,因為大小資訊已預先計算在資料結構中。
儘管 Elixir 包含特定函式,例如 tuple_size
、binary_size
和 map_size
,但有時我們希望能夠擷取資料結構的大小,而不論其類型。在 Elixir 中,我們可以使用協定來撰寫多型程式碼,亦即與不同形狀/類型的程式碼一起運作。大小協定可以實作如下
defprotocol Size do
@doc "Calculates the size (and not the length!) of a data structure"
def size(data)
end
現在協定可以實作於每個資料結構,協定可能會有一個相容的實作
defimpl Size, for: BitString do
def size(binary), do: byte_size(binary)
end
defimpl Size, for: Map do
def size(map), do: map_size(map)
end
defimpl Size, for: Tuple do
def size(tuple), do: tuple_size(tuple)
end
最後,我們可以使用 Size
協定來呼叫正確的實作
Size.size({1, 2})
# => 2
Size.size(%{key: :value})
# => 1
請注意,我們沒有對清單實作它,因為我們沒有清單的 size
資訊,而是需要使用 length
計算其值。
您實作協定的資料結構必須是協定中定義的所有函式的第一個參數。
可以實作所有 Elixir 類型的協定
協定和結構
協定的真正好處是在與結構混合時產生。例如,Elixir 附帶許多資料類型以結構實作,例如 MapSet
。我們也可以為這些類型實作 Size
協定
defimpl Size, for: MapSet do
def size(map_set), do: MapSet.size(map_set)
end
在為結構實作協定時,如果 defimpl/3
呼叫在定義結構的模組中,則可以省略 :for
選項
defmodule User do
defstruct [:email, :name]
defimpl Size do
# two fields
def size(%User{}), do: 2
end
end
如果未找到給定類型的協定實作,則呼叫協定會引發錯誤,除非將其設定為備援到 Any
。也提供在既有實作上建置實作的便利性,請參閱 defstruct/1
以取得關於衍生協定的更多資訊。
備援到 Any
在某些情況下,提供所有類型的預設實作可能會很方便。這可以透過在協定定義中將 @fallback_to_any
屬性設定為 true
來達成
defprotocol Size do
@fallback_to_any true
def size(data)
end
現在可以為 Any
實作 Size
協定
defimpl Size, for: Any do
def size(_), do: 0
end
儘管上述實作可以說是不可行的。例如,說 PID 或整數的大小為 0
沒有意義。這就是 @fallback_to_any
是選擇性行為的原因之一。對於大多數協定來說,在未實作協定時引發錯誤是適當的行為。
多重實作
協定也可以同時為多種類型實作
defprotocol Reversible do
def reverse(term)
end
defimpl Reversible, for: [Map, List] do
def reverse(term), do: Enum.reverse(term)
end
在 defimpl/3
內,您可以使用 @protocol
存取正在實作的協定,並使用 @for
存取為其定義的模組。
類型
定義協定會自動定義一個名為 t
的零元類型,如下所示
@spec print_size(Size.t()) :: :ok
def print_size(data) do
result =
case Size.size(data) do
0 -> "data has no items"
1 -> "data has one item"
n -> "data has #{n} items"
end
IO.puts(result)
end
上述 @spec
表示允許實作給定協定的所有類型都是給定函式的有效引數類型。
反射
任何協定模組都包含三個額外函式
__protocol__/1
- 傳回協定資訊。此函式採用下列其中一個原子:consolidated?
- 傳回協定是否已整合:functions
- 傳回協定函式及其元數的關鍵字清單:impls
- 如果已整合,傳回{:consolidated, modules}
,其中包含實作協定的模組清單;否則傳回:not_consolidated
:module
- 協定模組原子名稱
impl_for/1
- 傳回實作給定引數協定的模組,否則傳回nil
impl_for!/1
- 與上述相同,但如果找不到實作,則會引發Protocol.UndefinedError
例如,對於 Enumerable
協定,我們有
iex> Enumerable.__protocol__(:functions)
[count: 1, member?: 2, reduce: 3, slice: 1]
iex> Enumerable.impl_for([])
Enumerable.List
iex> Enumerable.impl_for(42)
nil
此外,每個協定實作模組都包含 __impl__/1
函式。此函式採用下列其中一個原子
:for
- 傳回負責協定實作資料結構的模組:protocol
- 傳回提供此實作的協定模組
例如,實作 Enumerable
協定的清單模組是 Enumerable.List
。因此,我們可以在此模組上呼叫 __impl__/1
iex(1)> Enumerable.List.__impl__(:for)
List
iex(2)> Enumerable.List.__impl__(:protocol)
Enumerable
整合
為了加速協定調度,每當所有協定實作都已預先得知(通常在專案中所有 Elixir 程式碼編譯後),Elixir 提供了一項稱為「協定整合」的功能。整合會以一種方式直接將協定連結至其實作,呼叫整合協定的函式等同於呼叫兩個遠端函式。
協定整合預設套用於編譯期間的所有 Mix 專案。這在測試期間可能是一個問題。例如,如果您想在測試期間實作一個協定,實作將不會有任何效果,因為協定已經整合。一個可能的解決方案是在 mix.exs 中包含特定於測試環境的編譯目錄
def project do
...
elixirc_paths: elixirc_paths(Mix.env())
...
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
然後,您可以在 test/support/some_file.ex
內定義特定於測試環境的實作。
另一種方法是在 mix.exs 中停用測試期間的協定整合
def project do
...
consolidate_protocols: Mix.env() != :test
...
end
如果您使用 Mix.install/2
,您可以透過傳遞 consolidate_protocols
選項來執行此動作
Mix.install(
deps,
consolidate_protocols: false
)
雖然這樣做不建議,因為它可能會影響程式碼的效能。
最後,請注意所有協定都編譯為 debug_info
設定為 true
,無論 elixirc
編譯器設定的選項為何。偵錯資訊用於合併,且在合併後會移除,除非已設定為全域。
摘要
函式
檢查指定的模組是否已載入,且為指定協定的實作。
檢查指定的模組是否已載入,且為協定。
接收協定和實作清單,並合併指定的協定。
如果協定已合併,則傳回 true
。
使用指定的選項為 module
衍生 protocol
。
從指定的路徑中萃取所有實作指定協定的類型。
從指定的路徑中萃取所有協定。
函式
檢查指定的模組是否已載入,且為指定協定的實作。
如果是,則傳回 :ok
,否則會引發 ArgumentError
。
@spec assert_protocol!(module()) :: :ok
檢查指定的模組是否已載入,且為協定。
如果是,則傳回 :ok
,否則會引發 ArgumentError
。
@spec consolidate(module(), [module()]) :: {:ok, binary()} | {:error, :not_a_protocol} | {:error, :no_beam_info}
接收協定和實作清單,並合併指定的協定。
合併是透過變更抽象格式中的協定 impl_for
來進行,以建立快速的查詢規則。通常會在合併期間使用的實作清單,會透過 extract_impls/2
的協助來擷取。
它會傳回協定位元組碼的更新版本。如果元組的第一個元素是 :ok
,表示協定已合併。
可以透過分析協定屬性來檢查指定的位元組碼或協定實作是否已合併
Protocol.consolidated?(Enumerable)
此函式在任何時間點都不會載入協定,也不會載入已編譯模組的新位元組碼。但是,每個實作都必須可用,而且會載入。
如果協定已合併,則傳回 true
。
使用指定的選項為 module
衍生 protocol
。
如果您的實作傳遞選項,或者您根據結構產生自訂程式碼,則您還需要實作定義為 __deriving__(module, struct, options)
的巨集,以取得已傳遞的選項。
範例
defprotocol Derivable do
def ok(arg)
end
defimpl Derivable, for: Any do
defmacro __deriving__(module, struct, options) do
quote do
defimpl Derivable, for: unquote(module) do
def ok(arg) do
{:ok, arg, unquote(Macro.escape(struct)), unquote(options)}
end
end
end
end
def ok(arg) do
{:ok, arg}
end
end
defmodule ImplStruct do
@derive [Derivable]
defstruct a: 0, b: 0
end
Derivable.ok(%ImplStruct{})
#=> {:ok, %ImplStruct{a: 0, b: 0}, %ImplStruct{a: 0, b: 0}, []}
現在可以透過 __deriving__/3
呼叫明確衍生
# Explicitly derived via `__deriving__/3`
Derivable.ok(%ImplStruct{a: 1, b: 1})
#=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, []}
# Explicitly derived by API via `__deriving__/3`
require Protocol
Protocol.derive(Derivable, ImplStruct, :oops)
Derivable.ok(%ImplStruct{a: 1, b: 1})
#=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, :oops}
從指定的路徑中萃取所有實作指定協定的類型。
路徑可以是字元清單或字串。內部會將它們視為字元清單處理,因此傳遞它們作為清單可以避免額外的轉換。
不會載入任何實作。
範例
# Get Elixir's ebin directory path and retrieve all protocols
iex> path = Application.app_dir(:elixir, "ebin")
iex> mods = Protocol.extract_impls(Enumerable, [path])
iex> List in mods
true
從指定的路徑中萃取所有協定。
路徑可以是字元清單或字串。內部會將它們視為字元清單處理,因此傳遞它們作為清單可以避免額外的轉換。
不會載入任何協定。
範例
# Get Elixir's ebin directory path and retrieve all protocols
iex> path = Application.app_dir(:elixir, "ebin")
iex> mods = Protocol.extract_protocols([path])
iex> Enumerable in mods
true