当前位置: 首页>>代码示例 >>用法及示例精选 >>正文


Elixir Protocol用法及代码示例


Elixir语言中 Protocol 相关用法介绍如下。

使用协议的参考和函数。

协议指定应由其实现定义的 API。协议用 Kernel.defprotocol/2 定义,其实现用 Kernel.defimpl/3 定义。

真实案例

在 Elixir 中,我们有两个名词用于检查数据结构中有多少项:lengthsizelength 表示必须计算信息。例如length(list)需要遍历整个列表来计算它的长度。另一方面,tuple_size(tuple)byte_size(binary) 不依赖于元组和二进制大小,因为大小信息是在数据结构中预先计算的。

尽管 Elixir 包含特定的函数,例如 tuple_sizebinary_sizemap_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 类型实现协议:

协议和结构

当与结构混合时,协议的真正好处就来了。例如,Elixir 附带了许多实现为结构的数据类型,例如 MapSet 。我们也可以为这些类型实现Size 协议:

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

在为结构实现协议时,如果 defimpl/3 调用位于定义结构的模块内,则可以省略 :for 选项:

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

现在可以为 Any 实现 Size 协议:

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 的 zero-arity 类型,可以按如下方式使用:

@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

合并

为了加快协议调度,只要知道所有协议实现up-front,通常在编译项目中的所有 Elixir 代码后,Elixir 都会提供一个名为 protocol consolidation 的函数。整合直接将协议链接到它们的实现,从整合协议中调用一个函数相当于调用两个远程函数。

默认情况下,协议合并在编译期间应用于所有 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 编译器设置的选项无关。调试信息用于合并,除非全局设置,否则在合并后将其删除。

相关用法


注:本文由纯净天空筛选整理自elixir-lang.org大神的英文原创作品 Protocol。非经特殊声明,原始代码版权归原作者所有,本译文未经允许或授权,请勿转载或复制。