當前位置: 首頁>>代碼示例 >>用法及示例精選 >>正文


Elixir Protocol用法及代碼示例


Elixir語言中 Protocol 相關用法介紹如下。

使用協議的參考和函數。

協議指定應由其實現定義的 API。協議用 Kernel.defprotocol/2 定義,其實現用 Kernel.defimpl/3 定義。

真實案例

在 Elixir 中,我們有兩個名詞用於檢查數據結構中有多少項:lengthsizelength 表示必須計算信息。例如length(list)需要遍曆整個列表來計算它的長度。另一方麵,tuple_size(tuple)byte_size(binary) 不依賴於元組和二進製大小,因為大小信息是在數據結構中預先計算的。

盡管 Elixir 包含特定的函數,例如 tuple_sizebinary_sizemap_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 信息,而是需要使用 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 的 zero-arity 類型,可以按如下方式使用:

@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

合並

為了加快協議調度,隻要知道所有協議實現up-front,通常在編譯項目中的所有 Elixir 代碼後,Elixir 都會提供一個名為 protocol consolidation 的函數。整合直接將協議鏈接到它們的實現,從整合協議中調用一個函數相當於調用兩個遠程函數。

默認情況下,協議合並在編譯期間應用於所有 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

盡管不建議這樣做,因為它可能會影響您的測試套件性能。

最後,請注意,所有協議都是在 debug_info 設置為 true 的情況下編譯的,而與 elixirc 編譯器設置的選項無關。調試信息用於合並,除非全局設置,否則在合並後將其刪除。

相關用法


注:本文由純淨天空篩選整理自elixir-lang.org大神的英文原創作品 Protocol。非經特殊聲明,原始代碼版權歸原作者所有,本譯文未經允許或授權,請勿轉載或複製。