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

欢迎!请查看 关于 页面了解更多关于这样工作的信息。

+11
协议
编辑

我正在做一个项目,这个项目关注于从多个来源和格式(如 xlsx、csv、jsons 等)集成数据。在通过实现第一个集成以及他们的通过 spec 变现验证后,我开始学习如何通过使用 protocolsdefrecords 来优化我的代码。

然而,我并不清楚 protocolscustom types 的使用场景。什么时候应该使用 protocolsdefrecords?有没有相关的建议?

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

我总是能看到关于 如何 实现或与 Java 接口进行一些非常表面的比较的教程,但关于如何将 protocolsrecords 用于应用程序中更大模式的材料却很少。

3 答案

+12

协议(和多态方法)是Clojure工具,为多态性、开放性和行为抽象提供支持。也就是说,调用者可以对不同的输入调用相同的操作,有些实现将会代表他们进行选择和调用。因为这些系统是开放的,所以可以在事实发生后进行扩展,而不需要修改调用者。

一般来说,每当您感觉到一个行为抽象,尤其是那些可能在以后用于扩展的抽象(尽管这不是必需的),那么您应该考虑协议和多态方法。这些工具有几个方面的区别——协议在第一个参数上执行快的类型派发,并且支持一组操作。多态方法对所有操作的参数执行基于值的派发(这包含了在第一个参数上的基于类型的派发)。在两者都可行的情况下,协议通常更快且更好。

一条关于协议的建议——协议函数作为SPI来连接实现通常比在API中将它们作为函数消费者直接调用的更好。通常,包装协议方法可以在需要时通过一个普通函数来提供额外的逻辑,这样做封装协议调用。我们的经验表明,这在架构上是成功的,并且代码在演变过程中表现良好。一个缺点是,这损害了协议相对于多态方法的优势,所以请仔细考虑这一点。

记录最常被比作有意场映射,用于信息用途。(deftype通常用于创建自定义结构,通常是不同或更底层的使用案例。)在比较时,有许多细微的权衡。总的来说,消费者通常以前后一致的方式与映射和记录交互(当记录实现映射接口时)。

它们的区别在于构建(记录有预构建的工厂方法,映射则没有),以及“类型”(生成的具体记录类),这使得它们可以被协议挂载。此外,在记录内植入协议实现的能够性,为连接到多态行为的信息映射(通过利用JVM在这个方面的极致优化的路径)提供了性能的最佳点。记录与映射的选择不是简单的——重要的是首先比较所有维度。

鉴于SPI涉及许多缩写,在这个上下文中澄清您在这里的SPI含义可能是一个好主意。
是的,谢谢。我的意思是“服务提供者接口”,基本上是您的组件需要从另一个地方获取的内容。
感谢详细的解释,非常有用。
+7

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

TL;DR(三行或更少阅读)

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

如果您的函数在运行时使用不同的事情调用,并且它所执行的操作必须专门针对调用它时所使用的事物。那么,您想要使用协议或多方法。如果可以通过根据第一个参数的类型来选择要做什么,则使用协议,否则,使用多方法。

如果您的函数应该根据 Map 的类型执行不同的操作。比如说,您需要将一个 Map 标记为表示某物,如 Person、Car、User 等。并且您有使用超过一种类型的 Maps 调用的函数,您希望它们根据这种类型来选择要做什么,那么请使用 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。一个记录实际上就是一个 Map,但是它的类型被记录类型名称所替换。所以,而不是有类型 Map,它将具有记录名称作为类型。

比如说,你想要一个类型为 Person 的 Map,你将使用 defrecord 完成。

这只有在需要一份代码在运行时根据类型做不同的事情时才真正有用。例如,如果有一段代码将要接收不同类型的 Maps,并且你希望它对于每个类型都做不同的事情,你还想要更好的性能。

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

defrecord 仅仅允许函数根据它们第一个参数的类型执行不同的操作,记录的结构和值不能决定其类型。这通常被称为名义类型。你的记录之所以属于某个类型,是因为你明确地给它起了那个名字。另一方面,defmulti 允许鸭子类型,即一个 Map 可以因为其内在的结构以及包含的值而具有某种类型。

defprotocol

当你想要创建基于其第一个参数原生类型的不同的函数时,你会使用 defprotocol。比如说,一个将要被不同的记录调用,并为每种不同的记录类型执行不同操作的函数。

defmulti

像我简要谈到的记录一样。如果你想要一个函数根据第一个参数的类型执行不同的操作,同时还因为其他参数的类型、数据的结构、任何参数的值,或者任何参数的值而执行不同的操作,你应该使用 defmulti 而不是 defprotocol。

多重参数

此外,如果你只是想让一个函数根据它被调用的参数数量做不同的事情,你可以使用多重参数函数。

by
太棒了。非常宝贵,我发现很少有帖子能够传递**隐性知识**(如 Zachary Tellman 所著 *Clojure 元素* 引言中描述的)。不能与经验丰富的程序员一起工作,会使这类过程的学习变得更困难。

感谢您的贡献
+1

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

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