Spec-Zone.ru › Elixir 1.18

Исходный код Протокол поведение

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

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

Настройка

Доступны следующие атрибуты модуля для настройки протокола:

  • @fallback_to_any - при значении true, разрешает переадресацию протокола к любому

  • @undefined_impl_description - строка с дополнительным описанием, которое будет использоваться в Protocol.UndefinedError, когда поиск реализации завершается неудачно. Эта опция применяется только если @fallback_to_any не установлена в true

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

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

Краткое описание

Обратные вызовы

__deriving__(модуль, термин)

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

__protocol__(атом)

Функция, доступная во всех определениях протоколов, которая возвращает метаданные протокола.

impl_for(термин)

Функция, доступная во всех определениях протоколов, которая возвращает реализацию для данного term или nil.

impl_for!(термин)

Функция, доступная во всех определениях протоколов, которая возвращает реализацию для данного term или выбрасывает исключение.

Функции

assert_impl!(протокол, база)

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

assert_protocol!(модуль)

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

consolidate(протокол, типы)

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

consolidated?(протокол)

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

derive(протокол, модуль, опции \\ [])

Выводит protocol для module с заданными опциями.

extract_impls(протокол, пути)

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

extract_protocols(пути)

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

END_OF_DOCUMENT_MARKER

Обратные вызовы

__deriving__(module, term)Source

@macrocallback __deriving__(module(), term()) :: Macro.t()

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

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

См. Protocol.derive/3 для примера.

__protocol__(atom)Source

@callback __protocol__(:consolidated?) :: boolean()
@callback __protocol__(:functions) :: [{atom(), arity()}]
@callback __protocol__(:impls) :: {:consolidated, [module()]} | :not_consolidated
@callback __protocol__(:module) :: module()

Функция, доступная во всех определениях протоколов, которая возвращает метаданные протокола.

impl_for(term)Source

@callback impl_for(term()) :: module() | nil

Функция, доступная во всех определениях протоколов, которая возвращает реализацию для данного term или nil.

Если @fallback_to_any равно true, nil никогда не возвращается.

impl_for!(term)Source

@callback impl_for!(term()) :: module()

Функция, доступная во всех определениях протоколов, которая возвращает реализацию для данного term или вызывает исключение.

Если @fallback_to_any равно true, исключение никогда не возникает.

Функции

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}

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

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

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

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

Protocol.consolidated?(Enumerable)

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

consolidated?(protocol)Source

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

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

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

Выводит реализацию protocol для module с заданными параметрами.

Каждый раз, когда вы выводите протокол, Elixir проверит, реализовал ли протокол обратный вызов Protocol.__deriving__/2. В случае успеха, обратный вызов будет вызван, и он должен определить модуль реализации. В противном случае реализация, просто указывающая на реализацию Any , автоматически выводится.

Примеры

defprotocol Derivable do
  @impl true
  defmacro __deriving__(module, options) do
    # If you need to load struct metadata, you may call:
    # struct_info = Macro.struct_info!(module, __CALLER__)

    quote do
      defimpl Derivable, for: unquote(module) do
        def ok(arg) do
          {:ok, arg, unquote(options)}
        end
      end
    end
  end

  def ok(arg)
end

После определения протокола существуют два способа его вывода. Первый - использование атрибута модуля @derive во время определения структуры:

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

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

Если структура уже определена, вы можете вызвать эту макрос:

require Protocol
Protocol.derive(Derivable, ImplStruct, :oops)
Derivable.ok(%ImplStruct{a: 1, b: 1})
#=> {:ok, %ImplStruct{a: 1, b: 1}, :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.36.1) для языка программирования Elixir

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

Spec-Zone.ru

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