请在 2024 年 Clojure 状态调查中分享您的想法!

欢迎!请参阅关于页面以了解更多有关此功能的信息。

+11
协议
编辑

我在一个项目上工作,该项目聚焦于从多个来源和格式(xlsx、csv、json等)进行数据集成。在实现了第一个集成及其利用 spec 的验证后,我开始研究如何通过使用 protocolsdefrecords 来改进我的代码。

然而,对于 protocolscustom types 的用例我还不是很清楚。我应该何时使用 protocolsdefrecords?有没有一些关于这个的建议?

因为,在我学习的过程中,面向抽象编程可以更好地实现代码的重用。但如何在代码中发现这些时刻呢?

我总是能找到有关如何实现的教程或者与Java接口的一些浅显比较,但关于如何使用 protocols和records来更好解决应用程序中的这些更大模式的资料却很少。

3 答案

+12

协议(和多方法)是Clojure提供多态、开放、行为抽象的工具。这意味着调用者可以对不同的输入调用相同的操作,并且会选择并代表调用者调用某些实现。因为这些系统是开放的,它们可以在之后进行扩展,而不会改变调用者。

通常情况下,每次你发现行为抽象,尤其是那些以后可能会用到的抽象(虽然这不是必须的),你应该考虑协议和多方法。这些工具在几个方面有所不同——协议在第一个参数上进行基于类型的快速调度,并支持一系列操作。多方法对所有单个操作的参数进行基于值的调度(这包括了第一个参数的基于类型的调度)。如果两种方法都可行,协议通常更快、更好。

一项关于协议的建议——协议函数作为SPI(服务提供者接口)来连接实现比作为API中直接调用的函数消费方更好。通常,用可以围绕调用协议的协议方法提供的附加逻辑的正常函数包裹协议方法非常有帮助。我们的经验表明,这种方法在架构上和代码随时间演化方面都非常出色。一个缺点是它会损害协议相对于多方法的性能优势,因此请仔细考虑。

记录在信息使用方面通常与具有已知字段的映射进行比较。(通常使用deftype来创建自定义结构,这通常是不同的、更低级别的用例。)与映射比较时,有许多细微的区别。一般来说,消费者通常以相同的方式与映射和记录交互(其中记录实现了映射接口)。

它们在构建方面通常不同(记录具有预构建的工厂方法,而映射没有),并且在拥有“类型”(生成的具体记录类)方面不同,这使得它们更适合通过协议连接。此外,将协议实现内联到记录中,针对特别与多态行为信息映射(通过利用JVM对此的高度优化的路径)的性能而言是一个完美的结合点。记录与映射并非简单的选择——在作出选择之前,比较所有维度很重要。

by
由于SPI指的是许多首字母缩略词,在此上下文中澄清你所说的SPI的含义可能是一个好主意。
by
是的,谢谢。我的意思是“服务提供者接口”,基本上是你组件从另一个地方需要的东西。
感谢详细的解释,非常有帮助。
+7

总的来说,你不太可能经常使用协议、记录和自定义类型。对于大多数应用域特定逻辑,使用普通映射和常规函数就足够了。不过,我会尽力解释何时需要使用它们。

TL;DR

使用 deftype 在需要实现需要内部可变性的自定义数据容器,或某种形式的数据封装时。这主要用于非常原始的结构,如数据结构或引用类型。

如果你有一个在运行时调用不同参数的函数,其行为必须特定于所调用的参数。那么你想使用协议或多方法。如果你可以仅基于第一个参数的类型来选择执行的操作,则使用协议;如果不可以,则使用多方法。

如果你有一个函数应该根据 Map 的类型执行不同的操作。比如说,你需要将 Map 标记为表示某个东西,比如人、车、用户等。你有一些将要使用不同类型 Map 的函数,你想要它们根据这种类型选择要做什么,那么可以使用 defrecord。

如果你有一个根据所调用参数的数量需要执行不同操作的函数,使用多参数就足够了。

最后,如果你发现自己写了很多以相同名称开始的函数,但以某种与类型相关的判别器结束,比如:`add-user`,`add-role`,`add-item`,这可能表明你可以用协议或多方法及其单个`add`函数来建模这一点。

deftype

让我从 deftype 开始。你使用 deftype 的频率最低。deftype 允许您实现新的抽象数据类型(ADT)。它用于创建新的数据容器类型。基本上,任何需要封装数据(通常是可变数据)并提供通过一个安全界面访问数据的方式,这个界面以对该类型有意义的方式强制执行底层数据的所有不变性。

例如,如果你需要添加一个新的数据结构,比如你需要实现一个双向链表,那么你将使用 deftype。

现在,您几乎永远不需要使用它,因为大部分有用的数据容器已经为您在核心、互通或作为库中实现了。例如,Java提供了一个双链表:[https://docs.oracle.com/javase/8/docs/api/java/util/LinkedList.html](https://docs.oracle.com/javase/8/docs/api/java/util/LinkedList.html)

为了更好地理解,deftype 允许数据封装。在一般情况下,在 Clojure 中,您不需要数据封装,因为默认的数据结构和绑定是不可变的,因此向外公开数据作为只读是没有危险的。话虽如此,对于实现数据容器本身,例如不可变的数据结构或者像 Atom 这样各种引用类型,您将需要变异来提供空间和时间效率高的实现。在这种情况下,您不希望外界随意触及数据,因为他们很容易破坏所需的不变性。因此,您想要提供一个抽象接口,例如,对于一个双链表,您可能会有 add-first(在前面添加)、add-last(在后面添加)、remove(移除)、next(下一个)、prev(上一个)等。因此,再次强调,deftype 对于将任何形式的数据容器添加到语言中非常有用,您几乎不必自己这样做。

defrecord

当您需要创建一个自定义类型以向某些 Map 添加语义意义时,您会使用 defrecord。实际上,Record 只是一个 Map,但其类型被替换为记录类型名。因此,它不会有 Map 类型,而是有记录名称作为类型。

例如,如果您想要一个类型为 Person 的 Map,您将使用 defrecord。

这仅在实际需要根据类型在运行时执行不同操作的情况下才有用。因此,如果有代码将接收不同类型的 Maps,并且您希望它对每种类型都执行不同的操作,同时您还想获得更好的性能。

我还想说,实际上有许多方法可以提供这种动态功能。一种是用原生类型,由记录和协议提供。另一种是手动建模的类型和 defmulti 的使用。后者更强大,因为您可以用您想要的任何东西来表示类型,类型甚至可以从数据本身的结构或值中推断出来。另一方面,它会更慢。

defrecord 只允许函数基于它们的第一个参数的类型执行不同的事情,并且记录的结构和值不能决定它的类型。这通常被称为名义类型。您的记录是某种类型,因为您明确这样命名了。另一方面,defmulti 允许鸭子类型,即 Map 可以根据其固有的结构及其包含的值具有某种类型。

defprotocol

当您需要函数基于它们第一个参数的本地类型执行不同的事情时,您将使用 defprotocol。例如,一个将使用不同记录并应根据记录类型执行不同操作的区域函数。

defmulti

正如我之前简要介绍记录时所说。如果您想使函数根据第一个参数的类型以及其他参数的类型、数据结构或任何参数的值执行不同的操作,您将使用 defmulti 而不是 defprotocol。

多态性

此外,如果您只想根据函数调用时传入的参数数量执行不同的操作,您可以使用多态函数。

by
太棒了。非常有价值,我发现很少帖子能够传递 **隐性知识**(正如Zachary Tellman在其《Clojure要素》引言中所描述的)。无法与经验丰富的程序员合作使得学习这类过程变得非常困难。

感谢您的贡献
+1
by

如果您想深入了解Clojure的多态性,我推荐Paul Stadig的书籍——《Clojure Polymorphism》。https://leanpub.com/clojurepolymorphism

by
我今天刚买了这本书。确实很好读,谢谢推荐
...