Spec-Zone.ru › Elixir 1.18

Исходный код Протоколы

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

defmodule Utility do
  def type(value) when is_binary(value), do: "string"
  def type(value) when is_integer(value), do: "integer"
  # ... other implementations ...
end

Если использование этого модуля ограничено вашим собственным проектом, вы сможете продолжать определять новые type/1 функции для каждого нового типа данных. Однако этот код может быть проблематичным, если он используется в качестве зависимости несколькими приложениями, потому что нет простого способа расширить его функциональность.

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

Вот как мы можем написать ту же самую Utility.type/1 функциональность в виде протокола:

defprotocol Utility do
  @spec type(t) :: String.t()
  def type(value)
end

defimpl Utility, for: BitString do
  def type(_value), do: "string"
end

defimpl Utility, for: Integer do
  def type(_value), do: "integer"
end

Мы определяем протокол с помощью defprotocol/2 — его функции и спецификации могут напоминать интерфейсы или абстрактные базовые классы в других языках. Мы можем добавить столько реализаций, сколько нам нужно, с помощью defimpl/2. Выходной результат точно такой же, как если бы у нас был единственный модуль с несколькими функциями:

iex> Utility.type("foo")
"string"
iex> Utility.type(123)
"integer"

Однако с протоколами мы больше не будем привязаны к необходимости постоянного изменения одного и того же модуля для поддержки все большего количества типов данных. Например, мы можем распределить defimpl вызовы выше по нескольким файлам, и Elixir будет перенаправлять выполнение на соответствующую реализацию в зависимости от типа данных. Функции, определенные в протоколе, могут иметь более одного входного значения, но диспетчеризация всегда будет основана на типе данных первого входного значения.

Один из наиболее распространённых протоколов, с которым вы можете столкнуться, — это String.Chars протокол: реализация его функции to_string/1 для ваших пользовательских структур сообщит ядру Elixir, как представлять их в виде строк. Мы изучим все встроенные протоколы позже. Пока давайте реализуем свой собственный.

Пример

Теперь, когда вы увидели пример того, какие проблемы помогают решить протоколы и как они это делают, давайте рассмотрим более подробный пример.

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

Даже если у нас есть специфичные для типа функции для получения размера, встроенные в Elixir (например, 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] of type List

Можно реализовать протоколы для всех типов данных Elixir:

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

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

Мощность расширяемости Elixir проявляется, когда протоколы и структуры используются вместе.

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

iex> Size.size(%{})
0
iex> set = %MapSet{} = MapSet.new
MapSet.new([])
iex> Size.size(set)
** (Protocol.UndefinedError) protocol Size not implemented for MapSet.new([]) of type MapSet (a struct)

Вместо того чтобы разделять реализацию протоколов с картами, структуры требуют собственной реализации протокола. Поскольку у 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

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

Вывод

Elixir позволяет нам вывести реализацию протокола на основе реализации Any. Давайте сначала реализуем Any следующим образом:

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

Эта реализация, вероятно, не является разумной. Например, бессмысленно говорить, что размер PID или Integer — это 0.

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

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

При выводе Elixir реализует протокол 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 — это опциональное поведение. Для большинства протоколов правильным поведением является возврат ошибки, если протокол не реализован. Тем не менее, предположим, что мы реализовали Any так же, как в предыдущем разделе:

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

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

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

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

Elixir поставляется с некоторыми встроенными протоколами. В предыдущих главах мы обсуждали модуль 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"

Обратите внимание, что интерполяция строк в Elixir вызывает функцию 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} of type Tuple

Когда требуется «вывести» более сложную структуру данных, можно использовать функцию 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}

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

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

Существуют и другие протоколы в Elixir, но это наиболее распространённые. Вы можете узнать больше о протоколах и реализациях в модуле Protocol.

← Предыдущая страница Структуры
Следующая страница → Понимания

Скачать версию ePub

Создано с помощью ExDoc (v0.36.1) для языка программирования Elixir

© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.18.1/protocols.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API