Do you want to pick up from where you left of?
Take me there

協定

在本課程中,將研究協定 (Protocols) 是什麼,以及如何在 Elixir 中使用。

Table of Contents

什麼是協定 (Protocols)

那麼協定是什麼?協定是實現 Elixir 多型 (polymorphism) 的一種手段。Erlang 的一個惱人之處是會為新定義的型別擴展現有 API。為了在 Elixir 中避免這件事,函數會根據值的型別動態調度 (dispatched dynamically)。

Elixir 帶有一些內建的協定,例如 String.Chars 協定負責之前看到並使用過的 to_string/1 函數。現在來看看 to_string/1 的一個簡短例子:

iex> to_string(5)
"5"
iex> to_string(12.4)
"12.4"
iex> to_string("foo")
"foo"

正如所看到的,我們已經在多種型別上呼用了函數,並證明它對所有型別都有效。如果在 tuple(或任何沒有實現 String.Chars 的型別)上呼用 to_string/1 會怎麼樣?這就來看看:

to_string({:foo})
** (Protocol.UndefinedError) protocol String.Chars not implemented for {:foo}
    (elixir) lib/string/chars.ex:3: String.Chars.impl_for!/1
    (elixir) lib/string/chars.ex:17: String.Chars.to_string/1

正如你所看到的,我們會得到一個協定錯誤 (protocol error),因為沒有實現 tuple。在下一節中,將為 tuple 實現 String.Chars 協定。

實現一個協定

我們看到 to_string/1 尚未因 tuple 實現 ,所以手動加進它。要建立一個實現,將在協定中使用 defimpl ,並提供 :for 選項和我們的型別。它可能看起來像:

defimpl String.Chars, for: Tuple do
  def to_string(tuple) do
    interior =
      tuple
      |> Tuple.to_list()
      |> Enum.map(&Kernel.to_string/1)
      |> Enum.join(", ")

    "{#{interior}}"
  end
end

如果將其複製到 IEx 中,現在應該可以在 tuple 上呼用 to_string/1 而不會出現錯誤訊息:

iex> to_string({3.14, "apple", :pie})
"{3.14, apple, pie}"

我們已經知道如何實現一個協定,但如何定義一個新協定呢?將實現 to_atom / 1 來做為範例。讓我們看看如何用 defprotocol 來做到這一點:

defprotocol AsAtom do
  def to_atom(data)
end

defimpl AsAtom, for: Atom do
  def to_atom(atom), do: atom
end

defimpl AsAtom, for: BitString do
  defdelegate to_atom(string), to: String
end

defimpl AsAtom, for: List do
  defdelegate to_atom(list), to: List
end

defimpl AsAtom, for: Map do
  def to_atom(map), do: List.first(Map.keys(map))
end

這裡定義了協定、它的預期函數 to_atom/1,以及幾種型別的實現。現在有了協定,接著在 IEx 中使用它:

iex> import AsAtom
AsAtom
iex> to_atom("string")
:string
iex> to_atom(:an_atom)
:an_atom
iex> to_atom([1, 2])
:"\x01\x02"
iex> to_atom(%{foo: "bar"})
:foo

值得注意的是,雖然下層結構是 Map,但它們不會與 Map 共享協定實現。它們不是可枚舉 (enumerable),也不能被存取。

而如以上所看到的,協定是實現多型的有效方式。

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!