Протоколы

Протоколы – это механизм для реализации полиморфизма в Эликсире. Обращение к протоколу доступно для любого типа данных, если этот тип реализует протокол. Давайте взглянем на пример.

В Эликсире есть два способа проверить, сколько экземпляров находится в структуре данных – функции length и size. Функция length предполагает, что информацию нужно вычислить. Например, length(list) должен пройтись по всему списку, чтобы вычислить его длину. С другой стороны, функции tuple_size(tuple) и byte_size(binary) просто берёт уже известный размер информации в кортеже или бинарных данных.

Даже если у нас есть встроенные в Эликсир функции для определённых типов, которые получают размер (такие как tuple_size/1), мы могли бы реализовать общий протокол Size, для всех структур данных, у которых размер подсчитан заранее.

Определение протокола бы выглядело подобным образом:

defprotocol Size do
  @doc "Calculates the size (and not the length!) of a data structure"
  def size(data)
end

Протокол Size ожидает, что есть функция size, которая принимает один аргумент (структуру данных, размер которой мы хотим узнать). Теперь мы можем реализовать этот протокол для структур данных:

defimpl Size, for: BitString do
  def size(string), do: byte_size(string)
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/1).

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

iex> Size.size("foo")
3

iex> Size.size({:ok, "hello"})
2

iex> Size.size(%{label: "some label"})
1

Попытка узнать размер типа данных, который не принимает этот протокол, вызовет ошибку:

iex> Size.size([1, 2, 3])
** (Protocol.UndefinedError) protocol Size not implemented for [1, 2, 3]

Протоколы можно применять для всех типов данных Эликсира:

  • Atom
  • BitString
  • Float
  • Function
  • Integer
  • List
  • Map
  • PID
  • Port
  • Reference
  • Tuple

Протоколы и структуры

Мощность расширямости Эликсира особенно хорошо проявляет себя при использовании протоколов и структур вместе.

В предыдущей главе мы узнали, что хотя структуры являются словарями, они не разделяют реализацию протоколов словарей. Например, модуль MapSet (множества на основе словарей) реализован как структура. Давайте попробуем использовать протокол Size для модуля MapSet:

iex> Size.size(%{})
0

iex> set = %MapSet{} = MapSet.new
#MapSet<[]>

iex> Size.size(set)
** (Protocol.UndefinedError) protocol Size not implemented for #MapSet<[]>

Вместо разделения реализации протокола со словарями, для структур необходима их собственная реализация протоколов. Т. к. размер MapSet подсчитан заранее и доступен через функцию MapSet.size/1, мы можем добавить его в протокол Size:

defimpl Size, for: MapSet do
  def size(set), do: MapSet.size(set)
end

Если хотите, вы можете добавить свою семантику для вычисления размера своих собственных структур. Кроме того, вы можете использовать структуры для создания более сложных типов данных, например, очередей, и реализовать для них все подходящие протоколы, такие как Enumerable и, возможно, Size.

defmodule User do
  defstruct [:name, :age]
end

defimpl Size, for: User do
  def size(_user), do: 2
end

Реализация типа Any

Ручная реализация протоколов для всех типов может быстро стать повторяющейся и утомительной. В таких случаях в Эликсире есть два варианта: избыточно получить реализацию протокола для все типов или автоматически применить протокол для всех типов. В обоих случаях нам нужно реализовать протокол через тип Any.

Извлечение протокола

Эликсир позволяет нам получить реализацию протокола, основанного на типе Any. Давайте сначала реализуем тип Any как показано ниже:

defimpl Size, for: Any do
  def size(_), do: 0
end

Вариант выше не самый лучший. Например, нет никакого смысла возвращать 0 как размер PID или Integer.

Однако, он имеет право на жизнь, т. к. при такой реализации типа Any нам нужно явно указывать в структурах извлечение протокола Size:

defmodule OtherUser do
  @derive [Size]
  defstruct [:name, :age]
end

При таком подходе Эликсир будет реализовывать протокол Size для модуля OtherUser, основываясь на реализации для Any.

Откат к Any

Другая альтернатива использованию @derive – явное объявление использования Any, когда не найдена другая реализация. Это можно сделать, установив значение переменной @fallback_to_any в true при определении протокола:

defprotocol Size do
  @fallback_to_any true
  def size(data)
end

Как мы сказали в предыдущем разделе, реализация протокола Size для типа Any не может быть применена ко всем типам данных. Это причина, по которой @fallback_to-any – опциональное поведение. Для большинства протоколов возникновение ошибки, когда протокол не реализован – наиболее подходящее решение. Давайте представим, что у нас есть подобная реализация протокола:

defimpl Size, for: Any do
  def size(_), do: 0
end

Теперь все типы данных (включая структуры), которые не реализуют протокол Size, будут возвращать 0 при запросе размера.

Какой из подходов лучше, извлечение протокола через @derive или откат к Any, зависит от вашей задачи. Но учитывая, что в разработке на Эликсире использовать явное считается лучше, чем неявное, во многих библиотеках вы можете увидеть выбор в пользу использования подхода с @derive.

Встроенные протоколы

В Эликсир изначально встроены некотороые протоколы. В предыдущих главах мы обсуждали модуль Enum, который предоставляет многие функции, которые работают в любой структуре данных, реализующей протокол Enumerable:

iex> Enum.map [1, 2, 3], fn(x) -> x * 2 end
[2, 4, 6]

iex> Enum.reduce 1..3, 0, fn(x, acc) -> x + acc end
6

Другой полезные пример – протокол String.Chars, который определяет, как конвертировать структуры с символами в строки. Это осуществляется функцией to_string:

iex> to_string :hello
"hello"

Обратите внимание, что интерполяция строк в Эликсире вызывает функцию to_string:

iex> "age: #{25}"
"age: 25"

Пример выше работает только потому, что числа реализуют протокол String.Chars. Передача кортежа, например, приведёт к ошибке:

iex> tuple = {1, 2, 3}
{1, 2, 3}

iex> "tuple: #{tuple}"
** (Protocol.UndefinedError) protocol String.Chars not implemented for {1, 2, 3}

Когда есть необходимость «напечатать» более сложную структуру данных, можно использовать функцию inspect, основанную на протоколе Inspect:

iex> "tuple: #{inspect tuple}"
"tuple: {1, 2, 3}"

Протокол Inspect – это протокол для трансформации любой структуры данных в читабельное текстовое предствление. Именно его инструменты вроде IEx используют для вывода результатов:

iex> {1, 2, 3}
{1, 2, 3}

iex> %User{}
%User{name: "john", age: 27}

Помните, что есть договорённость о выводе значений, начиная с #, если текстовое представление не соответствует синтаксису Эликсира. Это значит, что выведенная информация не обратима в часть кода и может быть потеряна:

iex> inspect &(&1+2)
"#Function<6.71889879/1 in :erl_eval.expr/5>"

В Эликсире есть и другие протоколы, но мы рассмотрели самые часто используемые.

Консолидация протоколов

При работе с проектами на Эликсире, использующими средство сборки Микс, вы можете увидеть подобный вывод:

Consolidated String.Chars
Consolidated Collectable
Consolidated List.Chars
Consolidated IEx.Info
Consolidated Enumerable
Consolidated Inspect

Это протоколы, которые входят в состав Эликсира и консолидируются в данном проекте. Т. к. протокол может быть применён к любому типу данных, он должен делать проверки при каждом вызове, существует ли реализация для нужного типа. Это может быть дорого.

Однако, после компиляции нашего проекта с использованием инструмента вроде Микса, мы знаем все модули, которые определены, включая протоколы и их реализации. Это значит, что протокол может быть консолидирован в очень простой и быстрый модуль исполнения.

Начиная с Эликсира версии 1.2, консолидация протоколов происходит автоматически для всех проектов. Мы будем осуществлять сборку нашего проекта в «Руководстве по Миксу и OTP».

© 2020 / Россия Любые мысли и вопросы пишите на elixir@wunsh.ru.