Spec-Zone.ru › Elixir 1.17

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

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

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

Spec-Zone.ru

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