Spec-Zone.ru › Elixir 1.16

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

Протоколы — это механизм достижения полиморфизма в 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.32.2) для языка программирования Elixir

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

Spec-Zone.ru

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