查看源代码 Protocol (Elixir v1.16.2)
关于使用协议工作的参考和函数。
协议指定了由其实现定义的 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 类型实现协议
协议和结构体
协议的真正好处在于与结构体混合使用。例如,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
的零元类型,可以按如下方式使用
@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
编译器设置的选项无关。调试信息用于整合,并且在整合后会删除,除非全局设置。
总结
函数
检查给定模块是否已加载,并且是给定协议的实现。
检查给定模块是否已加载,并且是协议。
接收协议和实现列表,并整合给定协议。
如果协议已整合,则返回 true
。
使用给定选项为 module
推断 protocol
。
从给定路径中提取为给定协议实现的所有类型。
从给定路径中提取所有协议。
函数
检查给定模块是否已加载,并且是给定协议的实现。
如果成功则返回 :ok
,否则引发 ArgumentError
。
@spec assert_protocol!(module()) :: :ok
检查给定模块是否已加载,并且是协议。
如果成功则返回 :ok
,否则引发 ArgumentError
。
@spec consolidate(module(), [module()]) :: {:ok, binary()} | {:error, :not_a_protocol} | {:error, :no_beam_info}
接收协议和实现列表,并整合给定协议。
整合通过将协议 impl_for
更改为抽象格式来实现快速查找规则。通常,整合期间使用的实现列表是通过 extract_impls/2
的帮助检索的。
它返回协议字节码的更新版本。如果元组的第一个元素是 :ok
,则表示协议已整合。
可以通过分析协议属性来检查给定的字节码或协议实现是否已整合。
Protocol.consolidated?(Enumerable)
此函数不会在任何时候加载协议,也不会加载编译模块的新字节码。但是,每个实现都必须可用,并且将被加载。
如果协议已整合,则返回 true
。
使用给定选项为 module
推断 protocol
。
如果您的实现传递了选项,或者您正在根据结构体生成自定义代码,那么您还需要实现一个定义为 __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}
从给定路径中提取为给定协议实现的所有类型。
路径可以是字符列表或字符串。它们在内部以字符列表的形式处理,因此将它们作为列表传递可以避免额外的转换。
不加载任何实现。
示例
# 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
从给定路径中提取所有协议。
路径可以是字符列表或字符串。它们在内部以字符列表的形式处理,因此将它们作为列表传递可以避免额外的转换。
不加载任何协议。
示例
# 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