请分享您的想法,并在 2024 Clojure 状态调查!

欢迎!有关如何工作的更多信息,请参阅 关于 页面。

+11
协议
编辑

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

然而,我并不清楚 procotolscustom types 的使用案例。我应该何时使用 protocolsdefrecords?在这方面有一些推荐吗?

因为我正在学习,面向抽象的编程使得代码重用更好。 但是如何捕捉这些时刻呢?

我总是发现有关如何实现的一些教程或与 Java 接口的一些非常肤浅的比较,但关于使用 protocolsrecords 解决的这些较大模式的应用,资料却不多。

3 答案

+12

协议(和多态方法)是Clojure提供的多态、开放的行为抽象工具。也就是说,调用者可以针对不同的输入调用相同操作,并且将选择某些实现来代表他们调用。因为这些系统是开放的,因此可以在事后扩展,而不需要改变调用者。

一般情况下,当你感觉到一个行为抽象时,尤其是那些以后很可能需要扩展的抽象(虽然这不是必需的),你应该看看协议和多态方法。这些工具有几个方面不同 - 协议可以在第一个参数上以类型为基础快速分配,并支持一组操作。多态方法对所有操作参数进行基于值的分配(这包含了在第一个参数上以类型为基础的分配)。在两种方式均可能的情况下,协议通常更快、更好。

一篇关于协议的建议 - 协议函数作为SPI更好地将实现连接进来,而不是在API中将它们作为调用者直接调用的函数。通常,包装协议方法在一个可以提供额外逻辑的普通函数中非常有帮助,这可以在调用协议之前和之后进行。我们的经验表明,这种方式在架构上是很好的,并且随着代码的演变也很有效。一个缺点是,它会损害协议相对于多态方法的性能优势,因此请仔细考虑这一点。

记录在信息使用上通常与具有已知字段的映射比较相似。(通常使用 deftype 来创建自定义结构,这是一种更低级的不同使用情况。)当与映射进行比较时,有许多细微的权衡。通常,消费者与映射和记录以相同的方式进行互动(其中记录实现了映射接口)。

它们在构造上有所不同(记录有预构建的工厂方法,而映射中没有),并且有“类型”(生成的具体记录类),这使得它们容易通过协议来连接。此外,在记录内部内联协议实现的能力,在针对与多态行为(通过利用JVM为此提供的优化路径)相连接的信息映射的情况下,使性能达到一个甜蜜点。记录与映射的选择不是简单的选择 - 首先比较所有维度很重要。

by
鉴于SPI涉及这么多首字母缩略词,在这个上下文中澄清SPI在这里的意思可能是个好主意。
by
是的,谢谢。我的意思是“服务提供者接口”,基本上是你的组件需要从其他组件那里得到的东西。
感谢详细的解释,非常有用。
+7

总的来说,您不太常用协议、记录和自定义类型。使用普通地图和常规函数对于大多数应用程序领域的特定逻辑就足够了。不过,我将尝试解释在什么情况下您会想要使用它们。

TL;DR

当您需要实现自定义数据容器,需要底层的可变性和/或某种形式的数据封装时,可以使用 deftype。这主要用于类似数据结构或引用类型这样的非常原始的结构。

如果您有一个在运行时调用不同内容的函数,并且它执行的操作必须针对其被调用的内容特定。那么您想使用协议或多方法。如果您可以通过选择第一个参数的类型来确定要执行的操作,请使用协议,否则请使用多方法。

如果您有一个函数应根据地图的类型执行不同的事情。比如说,您需要将地图标记为表示某个东西,比如人、车、用户等。并且您有需要用类型更多的地图调用的函数,并且希望根据该类型选择执行的操作,请使用 defrecord。

如果您有一个函数需要根据它被调用的参数数量执行某些操作,请使用多参数。

最后,如果您发现自己正在编写许多以相同名称开始但以某种与所调用内容类型相关的辨别器结束的函数。比如:添加用户添加角色添加条目,这可能表明您可以使用协议或多方法以及单个添加函数来建模这种情况。

deftype

我将从 deftype 开始。您最不经常使用 deftype。deftype 允许您实现新的抽象数据类型(ADT)。它对于创建新的数据容器类型很有用。基本上,任何需要封装数据(通常是可变数据),并通过一个安全接口以任何合理的方式为该类型强制所有底层数据不变性的东西都可以使用它。

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

现在,你几乎不需要使用它,因为大多数有用的数据容器已经在核心、互操作性层或者是作为库为您实现。例如,Java已经提供了双链表:https://docs.oracle.com/javase/8/docs/api/java/util/LinkedList.html

为了更好地理解,请考虑deftype允许数据封装。通常情况下,在Clojure中您不需要数据封装,因为默认的数据结构和绑定是不可变的,因此将数据以只读形式提供给外部没有危险。但即便如此,对于实现数据容器本身,例如不可变数据结构或者各种引用类型(如Atom),您需要使用变异以实现空间和时间的有效实现。在这种情况下,您不应让外部可以随意访问数据,因为它们可能会轻易破坏所需的不变性。因此,您希望提供一个抽象接口,例如对于双链表,您可能会有添加第一个元素、添加最后一个元素、删除、下一个、前一个等操作。所以,一遍又一遍地,deftype对于向语言添加任何形式的数据容器都是有用的,几乎不需要自己去做。

defrecord

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

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

这只有在您需要一段代码在运行时根据类型执行不同的操作时才真正有用。所以如果有一段代码将被不同类型的Map接收,并且希望它对每种类型都执行不同的操作,同时还想获得更好的性能。

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

defrecord只能允许函数根据它们第一个参数的类型执行不同的事情,而记录的结构和值不能决定其类型。这通常被称为命名类型。您的记录是某种类型,因为您明确地给出了这种类型的名字。另一方面,defmulti允许使用鸭式类型,即一个Map因其固有结构或包含的值而具有某种类型。

defprotocol

当您想要函数根据它们第一个参数的原生类型执行不同的事情时,请使用defprotocol。比如一个会被不同的记录调用,并根据每个不同的记录类型执行不同操作的功能。

defmulti

正如我在简要介绍记录时解释的那样。如果您希望一个函数不仅根据第一个参数的类型执行不同的操作,而且还因为其他参数的类型或任何参数中数据结构,或者任何参数的值而执行不同的操作,那么您将使用defmulti而不是defprotocol。

多参数函数

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

by
惊人的。非常有价值,我发现很少有帖子能够传达**隐含知识**(正如扎基·特区曼在《Clojure元素》引言中所描述的)。无法与经验丰富的程序员合作会使学习这类过程变得非常困难。

感谢您的贡献
+1
by

如果您想深入了解Clojure中的多态性,我推荐保罗·斯塔迪格的书——《Clojure Polymorphism》。https://leanpub.com/clojurepolymorphism

by
今天刚刚买了一本书。确实是一本好书,感谢你的建议
...