Elixir学习笔记——协议

协议是一种在 Elixir 中实现多态性的机制,您希望行为根据数据类型而变化。我们已经熟悉解决此类问题的一种方法:通过模式匹配和保护子句。考虑一个简单的实用程序模块,它会告诉我们输入变量的类型:

如果此模块的使用仅限于您自己的项目,您将能够继续为每种新数据类型定义新的 type/1 函数。但是,如果此代码作为依赖项由多个应用程序共享,则可能会出现问题,因为没有简单的方法来扩展其功能。

这就是协议可以帮助我们的地方:协议允许我们根据需要扩展尽可能多的数据类型的原始行为。这是因为协议上的调度适用于已实现协议的任何数据类型,并且任何人都可以随时实现协议。

以下是我们如何将相同的 Utility.type/1 功能编写为协议:

我们使用 defprotocol/2 定义协议 - 其功能和规范可能看起来类似于其他语言中的接口或抽象基类。我们可以使用 defimpl/2 添加任意数量的实现。输出与我们拥有一个具有多个函数的模块完全相同:

但是,使用协议,我们不再需要不断修改同一模块以支持越来越多的数据类型。例如,我们可以将上面的 defimpl 调用分散到多个文件中,Elixir 将根据数据类型将执行分派到适当的实现。协议中定义的函数可能有多个输入,但分派将始终基于第一个输入的数据类型。

您可能遇到的最常见的协议之一是 String.Chars 协议:为您的自定义结构实现其 to_string/1 函数将告诉 Elixir 内核如何将它们表示为字符串。我们稍后将探索所有内置协议。现在,让我们实现我们自己的协议。

例子

现在您已经看到了协议有助于解决的问题类型及其解决方式的示例,让我们看一个更深入的示例。

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

即使我们在 Elixir 中内置了用于获取大小的类型特定函数(例如 tuple_size/1),我们也可以实现一个通用的 Size 协议,所有预先计算大小的数据结构都会实现该协议。

协议定义如下:

Size 协议需要一个名为 size 的函数,该函数接收一个参数(我们想知道其大小的数据结构)。我们现在可以为具有兼容实现的数据结构实现此协议:

我们没有为列表实现 Size 协议,因为列表没有预先计算的“大小”信息,并且必须计算列表的长度(使用 length/1)。

现在有了定义的协议和实现,我们可以开始使用它了:

传递未实现协议的数据类型会引发错误:

可以为所有 Elixir 数据类型实现协议:

协议和结构

当协议和结构一起使用时,Elixir 的可扩展性就会发挥出来。

在上一章中,我们了解到,尽管结构是映射,但它们并不与映射共享协议实现。例如,MapSet(基于映射的集合)是作为结构实现的。让我们尝试将 Size 协议与 MapSet 一起使用:

结构不需要与映射共享协议实现,而是需要自己的协议实现。由于 MapSet 的大小是预先计算的,并且可以通过 MapSet.size/1 访问,因此我们可以为其定义一个 Size 实现:

如果需要,您可以为结构的大小提出自己的语义。不仅如此,您还可以使用结构来构建更强大的数据类型(如队列),并为这种数据类型实现所有相关协议(如 Enumerable 和可能的 Size)。

任意实现(Any)

手动实现所有类型的协议很快就会变得重复而乏味。在这种情况下,Elixir 提供了两种选择:我们可以明确派生我们类型的协议实现,或者自动实现所有类型的协议。在这两种情况下,我们都需要为 Any 实现协议。

派生

Elixir 允许我们基于 Any 实现派生协议实现。让我们首先按如下方式实现 Any:

上述实现可能不合理。例如,说 PID 或 Integer 的大小为 0 是没有意义的。

但是,如果我们对 Any 的实现满意,为了使用这种实现,我们需要告诉我们的结构明确派生 Size 协议:

在派生时,Elixir 将根据为 Any 提供的实现为 OtherUser 实现 Size 协议。

回退到 Any

@derive 的另一种替代方法是明确告知协议在找不到实现时回退到 Any。这可以通过在协议定义中将 @fallback_to_any 设置为 true 来实现:

正如我们在上一节中所说,Any 的 Size 实现不能应用于任何数据类型。这就是 @fallback_to_any 是可选行为的原因之一。对于大多数协议,当协议未实现时引发错误是正确的行为。也就是说,假设我们已经像上一节中那样实现了 Any:

现在,所有未实现 Size 协议的数据类型(包括结构)都将被视为具有 0 的大小。

在派生和回退到 Any 之间哪种技术最好取决于用例,但鉴于 Elixir 开发人员更喜欢显式而不是隐式,您可能会看到许多库都在推行 @derive 方法。

内置协议

Elixir 附带一些内置协议。在前面的章节中,我们讨论了 Enum 模块,它提供了许多可与实现 Enumerable 协议的任何数据结构配合使用的函数:

另一个有用的例子是 String.Chars 协议,它指定如何将数据结构转换为其以字符串形式表示的形式。它通过 to_string 函数公开:

请注意,Elixir 中的字符串插值调用 to_string 函数:

上面的代码片段只适用于数字实现 String.Chars 协议的情况。例如,传递元组将导致错误:

当需要“打印”更复杂的数据结构时,可以使用基于 Inspect 协议的 inspect 函数:

Inspect 协议是用于将任何数据结构转换为可读文本表示的协议。这就是 IEx 等工具用来打印结果的方式:

请记住,按照惯例,只要检查的值以 # 开头,它就表示非有效 Elixir 语法的数据结构。这意味着检查协议不可逆,因为信息可能会在此过程中丢失:

Elixir 中还有其他协议,但本文涵盖了最常见的协议。您可以在协议模块中了解有关协议和实现的更多信息。

  • 16
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值