Протоколы – это механизм для реализации полиморфизма в Эликсире. Обращение к протоколу доступно для любого типа данных, если этот тип реализует протокол. Давайте взглянем на пример.
В Эликсире есть два способа проверить, сколько экземпляров находится в структуре данных – функции 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».