Spec-Zone.ru › Elixir 1.17

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

Справочник и функции для работы с протоколами.

Протокол определяет API, который должны реализовывать его реализации. Протокол определяется с помощью Kernel.defprotocol/2, а его реализации с помощью Kernel.defimpl/3.

Практический пример

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

Хотя Elixir включает специфические функции, такие как tuple_size, binary_size и map_size, иногда нам нужно получить размер структуры данных независимо от её типа. В Elixir мы можем написать полиморфный код, т.е. код, работающий с различными формами/типами, используя протоколы. Протокол размера можно реализовать следующим образом:

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

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

defimpl Size, for: BitString do
  def size(binary), do: byte_size(binary)
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 для вызова соответствующей реализации:

Size.size({1, 2})
# => 2
Size.size(%{key: :value})
# => 1

Обратите внимание, что мы не реализовали его для списков, поскольку у списков нет size информации, а её значение необходимо вычислить с помощью length.

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

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

  • Структуры (см. раздел "Протоколы и структуры" ниже)
  • Tuple
  • Atom
  • List
  • BitString
  • Integer
  • Float
  • Function
  • PID
  • Map
  • Port
  • Reference
  • Any (см. раздел "Возврат к Any" ниже)

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

Настоящая польза протоколов проявляется при их использовании совместно со структурами. Например, Elixir поставляется с множеством типов данных, реализованных как структуры, таких как MapSet. Мы также можем реализовать протокол Size для этих типов:

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

При реализации протокола для структуры, опцию :for можно опустить, если вызов defimpl/3 находится в модуле, который определяет структуру:

defmodule User do
  defstruct [:email, :name]

  defimpl Size do
    # two fields
    def size(%User{}), do: 2
  end
end

Если реализация протокола не найдена для данного типа, вызов протокола вызовет исключение, если не настроено обращение к Any. Также доступны средства для создания реализаций на основе существующих. Обратитесь к defstruct/1 для получения дополнительной информации о производных протоколах.

Возврат к Any

В некоторых случаях удобно предоставить реализацию по умолчанию для всех типов. Это можно сделать, установив атрибут @fallback_to_any в значение true в определении протокола:

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

Протокол Size теперь можно реализовать для Any:

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

Хотя приведенная выше реализация, вероятно, не является разумной. Например, не имеет смысла говорить, что PID или целое число имеют размер 0. Это одна из причин, почему @fallback_to_any является поведенческим выбором. Для большинства протоколов поднятие ошибки при отсутствии реализации протокола — правильное поведение.

Несколько реализаций

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

defprotocol Reversible do
  def reverse(term)
end

defimpl Reversible, for: [Map, List] do
  def reverse(term), do: Enum.reverse(term)
end

Внутри defimpl/3 можно использовать @protocol для доступа к реализуемому протоколу и @for для доступа к модулю, для которого он определён.

Типы

Определение протокола автоматически определяет тип нулевой арности, названный t, который можно использовать следующим образом:

@spec print_size(Size.t()) :: :ok
def print_size(data) do
  result =
    case Size.size(data) do
      0 -> "data has no items"
      1 -> "data has one item"
      n -> "data has #{n} items"
    end

  IO.puts(result)
end

@spec выше выражает, что все типы, разрешённые для реализации данного протокола, являются допустимыми типами аргументов для данной функции.

Рефлексия

Любой модуль протокола содержит три дополнительные функции:

  • __protocol__/1 — возвращает информацию о протоколе. Функция принимает один из следующих атомов:

    • :consolidated? — возвращает, является ли протокол сконсолидированным

    • :functions — возвращает список функций протокола и их арности

    • :impls — если сконсолидирован, возвращает {:consolidated, modules} со списком модулей, реализующих протокол, в противном случае :not_consolidated

    • :module — имя атома модуля протокола

  • impl_for/1 — возвращает модуль, реализующий протокол для данного аргумента, nil в противном случае

  • impl_for!/1 — то же самое, что и выше, но вызывает исключение Protocol.UndefinedError, если реализация не найдена

Например, для протокола Enumerable мы имеем:

iex> Enumerable.__protocol__(:functions)
[count: 1, member?: 2, reduce: 3, slice: 1]

iex> Enumerable.impl_for([])
Enumerable.List

iex> Enumerable.impl_for(42)
nil

Кроме того, каждый модуль реализации протокола содержит функцию __impl__/1. Функция принимает один из следующих атомов:

  • :for — возвращает модуль, ответственный за структуру данных реализации протокола

  • :protocol — возвращает модуль протокола, для которого предоставляется эта реализация

Например, модуль, реализующий протокол Enumerable для списков, — Enumerable.List. Поэтому мы можем вызвать __impl__/1 в этом модуле:

iex(1)> Enumerable.List.__impl__(:for)
List

iex(2)> Enumerable.List.__impl__(:protocol)
Enumerable

Консолидация

Для ускорения диспетчеризации протоколов, когда все реализации протоколов известны заранее, обычно после компиляции всего кода Elixir в проекте, Elixir предоставляет функцию консолидации протоколов. Консолидация напрямую связывает протоколы с их реализациями таким образом, что вызов функции из сконсолидированного протокола эквивалентен вызову двух удалённых функций.

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

def project do
  ...
  elixirc_paths: elixirc_paths(Mix.env())
  ...
end

defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]

Затем вы можете определить реализации, специфичные для тестовой среды, внутри test/support/some_file.ex.

Другой подход — отключить консолидацию протоколов во время тестов в вашем mix.exs:

def project do
  ...
  consolidate_protocols: Mix.env() != :test
  ...
end

Если вы используете Mix.install/2, вы можете сделать это, передав опцию consolidate_protocols:

Mix.install(
  deps,
  consolidate_protocols: false
)

Хотя это не рекомендуется, так как может повлиять на производительность вашего кода.

Наконец, обратите внимание, что все протоколы компилируются с debug_info установленным в true, независимо от опции, установленной компилятором elixirc. Информация отладки используется для консолидации и удаляется после консолидации, если это не установлено глобально.

Резюме

Функции

assert_impl!(protocol, base)

Проверяет, загружен ли данный модуль и является ли он реализацией данного протокола.

assert_protocol!(module)

Проверяет, загружен ли данный модуль и является ли он протоколом.

consolidate(protocol, types)

Получает протокол и список реализаций и консолидирует данный протокол.

consolidated?(protocol)

Возвращает true если протокол был сконсолидирован.

derive(protocol, module, options \\ [])

Производит вычисление protocol для module с указанными опциями.

extract_impls(protocol, paths)

Извлекает все типы, реализованные для данного протокола из указанных путей.

extract_protocols(paths)

Извлекает все протоколы из указанных путей.

END_OF_DOCUMENT_MARKER

Функции

assert_impl!(protocol, base)Source

@spec assert_impl!(module(), module()) :: :ok

Проверяет, загружен ли данный модуль и является ли он реализацией заданного протокола.

Возвращает :ok в случае успеха, в противном случае генерирует исключение ArgumentError.

assert_protocol!(module)Source

@spec assert_protocol!(module()) :: :ok

Проверяет, загружен ли данный модуль и является ли он протоколом.

Возвращает :ok в случае успеха, в противном случае генерирует исключение ArgumentError.

consolidate(protocol, types)Source

@spec consolidate(module(), [module()]) ::
  {:ok, binary()} | {:error, :not_a_protocol} | {:error, :no_beam_info}

Принимает протокол и список реализаций и консолидирует заданный протокол.

Консолидация происходит путем изменения протокола impl_for в абстрактном формате для ускорения поиска. Обычно список реализаций для консолидации извлекается с помощью extract_impls/2.

Возвращает обновленную версию байткода протокола. Если первый элемент кортежа равен :ok, это означает, что протокол был консолидирован.

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

Protocol.consolidated?(Enumerable)

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

consolidated?(protocol)Source

@spec consolidated?(module()) :: boolean()

Возвращает true если протокол был сконсолидирован.

derive(protocol, module, options \\ [])Source

Вычисляет protocol для module с заданными параметрами.

Если ваша реализация передает параметры или вы генерируете пользовательский код на основе структуры, вам также необходимо реализовать макрос, определенный как __deriving__(module, struct, options) для получения переданных параметров.

Примеры

defprotocol Derivable do
  def ok(arg)
end

defimpl Derivable, for: Any do
  defmacro __deriving__(module, struct, options) do
    quote do
      defimpl Derivable, for: unquote(module) do
        def ok(arg) do
          {:ok, arg, unquote(Macro.escape(struct)), unquote(options)}
        end
      end
    end
  end

  def ok(arg) do
    {:ok, arg}
  end
end

defmodule ImplStruct do
  @derive [Derivable]
  defstruct a: 0, b: 0
end

Derivable.ok(%ImplStruct{})
#=> {:ok, %ImplStruct{a: 0, b: 0}, %ImplStruct{a: 0, b: 0}, []}

Явные вычисления теперь можно вызвать через __deriving__/3:

# Explicitly derived via `__deriving__/3`
Derivable.ok(%ImplStruct{a: 1, b: 1})
#=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, []}

# Explicitly derived by API via `__deriving__/3`
require Protocol
Protocol.derive(Derivable, ImplStruct, :oops)
Derivable.ok(%ImplStruct{a: 1, b: 1})
#=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, :oops}

extract_impls(protocol, paths)Source

@spec extract_impls(module(), [charlist() | String.t()]) :: [atom()]

Извлекает все типы, реализующие данный протокол, из заданных путей.

Пути могут быть строкой или списком. Внутри они обрабатываются как списки символов, поэтому передача их как списков избегает лишнего преобразования.

Не загружает никакие реализации.

Примеры

# Get Elixir's ebin directory path and retrieve all protocols
iex> path = Application.app_dir(:elixir, "ebin")
iex> mods = Protocol.extract_impls(Enumerable, [path])
iex> List in mods
true

extract_protocols(paths)Source

@spec extract_protocols([charlist() | String.t()]) :: [atom()]

Извлекает все протоколы из заданных путей.

Пути могут быть строкой или списком. Внутри они обрабатываются как списки символов, поэтому передача их как списков избегает лишнего преобразования.

Не загружает ни один из протоколов.

Примеры

# Get Elixir's ebin directory path and retrieve all protocols
iex> path = Application.app_dir(:elixir, "ebin")
iex> mods = Protocol.extract_protocols([path])
iex> Enumerable in mods
true

Скачать версию 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/Protocol.html

Spec-Zone.ru

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