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

Протоколы

В этом уроке мы рассмотрим протоколы в языке Elixir: что это такое и как их использовать.

Что такое протоколы?

Так что же это такое? Протоколы — способ реализации полиморфизма в Elixir. Исторически сложилось, что в Erlang есть проблема с расширением уже существующих API для новых типов. Решением этой проблемы в Elixir являются протоколы — функции, определяемые динамически на основе типа передаваемого значения.

В Elixir уже есть набор встроенных протоколов с имплементациями. Отличным примером является протокол String.Chars, который используется в функции 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

Как мы видим, возникает ошибка из-за отсутствия реализации для этого типа.

Имплементация протокола

Мы уже видели, что 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

Структуры реализованы как ассоциативные массивы, однако они не могут использовать те же имплементации протоколов, потому что не являются перечисляемыми.

Как мы убедились в этом уроке, протоколы являются мощным инструментом для достижения полиморфизма.

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