Spec-Zone.ru › Elixir 1.9

Протокол

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

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

Примеры

В 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 информации, а её значение необходимо вычислить с помощью length.

Возможна реализация протоколов для всех типов Elixir:

  • Структуры (см. ниже)
  • Tuple
  • Atom
  • List
  • BitString
  • Integer
  • Float
  • Function
  • PID
  • Map
  • Port
  • Reference
  • Any (см. ниже)

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

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

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

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

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/2 можно использовать @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 - аналогично выше, но генерирует ошибку, если реализация не найдена

Например, для протокола 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

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

Для обработки загрузки кода в режиме разработки, 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

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

Наконец, обратите внимание, что все протоколы компилируются с 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)

Спецификации

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

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

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

assert_protocol!(module)

Спецификации

assert_protocol!(module()) :: :ok

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

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

consolidate(protocol, types)

Спецификации

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

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

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

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

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

Protocol.consolidated?(Enumerable)

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

consolidated?(protocol)

Спецификации

consolidated?(module()) :: boolean()

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

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

Выводит 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__:

# Explicitly derived via `__deriving__`
Derivable.ok(%ImplStruct{a: 1, b: 1})

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

extract_impls(protocol, paths)

Спецификации

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

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

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

Не загружает ни одну из реализаций.

Примеры

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

extract_protocols(paths)

Спецификации

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

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

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

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

Примеры

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

© 2012 Plataformatec
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.9.4/Protocol.html

Spec-Zone.ru

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