Исходный код Протокол
Справочник и функции для работы с протоколами.
Протокол определяет 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:
- Структуры (см. раздел "Протоколы и структуры" ниже)
TupleAtomListBitStringIntegerFloatFunctionPIDMapPortReference-
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!(протокол, база)
Проверяет, загружен ли указанный модуль и является ли он реализацией данного протокола.
- assert_protocol!(модуль)
Проверяет, загружен ли указанный модуль и является ли он протоколом.
- consolidate(протокол, типы)
Принимает протокол и список реализаций и консолидирует данный протокол.
- consolidated?(протокол)
Возвращает
trueесли протокол был консолидирован.- derive(протокол, модуль, опции \\ [])
Выводит
protocolдляmoduleс заданными параметрами.- extract_impls(протокол, пути)
Извлекает все типы, реализующие данный протокол, из указанных путей.
- extract_protocols(пути)
Извлекает все протоколы из указанных путей.
Функции
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
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.16.3/Protocol.html