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

协议

我们将在本课程看看 Elixir 里面的协议到底是什么,以及如何使用。

Table of Contents

什么是协议

协议到底是什么呢?协议是 Elixir 实现多态的一种方式。Erlang 的其中一个痛点,就是为现有的 API 扩展到新定义的类型上面。为了在 Elixir 里解决这个问题,函数是基于数值的类型来动态派遣的。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"

正如你看到的,我们在函数调用的时候传入好几个不同类型的数据,而且也都能正常执行。那如果我们在调用 to_string/1 的时候传入元组(或者其它没有实现 String.Chars 协议的类型)呢?来看看结果:

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

由于没有为元组提供这个协议的实现,一个协议错误产生了。下一节,我们将为元组提供 String.Chars 协议的实现。

实现协议

我们已经知道,to_string/1 并没有为元组提供实现,所以让我们来添加一个。要提供某个协议的实现,我们需要使用 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,我们现在就可以在调用 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),但是它们并不和映射共享协议的实现方式。它们不可遍历,所以也不能以这种方式访问。

如你所见,协议是一种强大的实现多态的手段。

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