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

欢迎!请查看关于页面了解这个平台的一些基本信息。

+11
协议
编辑

我在进行一个项目,这个项目的重点是集成来自多个来源和格式的数据(xlsx, csv, jsons, 等)。在实现了第一个集成和一些利用spec的验证后,我开始学习如何通过使用prototcolsdefrecords来改进我的代码。

然而,关于如何使用prototcolscustom types并不明确。应在什么情况下使用protocolsdefrecords?有关于这些建议吗?

因为,正如我所说的,面向抽象的编程可以使代码重用更好。但是如何找到这些时刻呢?

我总是能找到有关如何实现或与Java接口进行某些非常浅薄比较的教程,但关于在您的应用中可能更适合用protocolsrecords来解决的问题的这些大的模式并没有多少材料。

3 答案

+12

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

通常,每次当你感受到行为抽象时,尤其是将来可能有用处扩展的行为抽象(尽管这并非必需),那么你应该考虑使用协议和多方法。这些工具在几个方面有所不同——协议在第一个参数上执行快速的基于类型的调度,并支持多个操作组。多方法对单个操作的参数执行基于值的调度(这包括在第一个参数上的基于类型的调度)。在两者都可行的情况下,协议通常更快、更好。

一项关于协议的建议——作为SPI(服务提供者接口)钩入实现比在API中将它们作为函数消费者直接调用更好。通常在调用协议方法之前使用一个普通的函数可以提供额外的逻辑,如果有需要的话。我们的经验表明,这从架构和代码随时间演变的角度来看都很成功。一个缺点是这会影响协议相对于多方法的性能优势,因此需要仔细考虑。

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

它们在构建上有所不同(记录具有预先构建的工厂方法,而映射没有),以及拥有一个“类型”(生成的具体记录类),这使得它们易于通过协议连接。此外,可以在记录内内联协议实现,这对于特别关联到多态行为的(通过利用JVM中为此优化的路径)信息映射的性能来说是一个理想的结合点。记录与映射的选择不是一个简单的选择——首先比较所有维度非常重要。

by
鉴于SPI指涉了如此多的首字母缩略词,在这种情况下澄清你在这里所说的SPI可能是个好主意。
by
是的,谢谢。我的意思是“服务提供者接口”,基本上是指你的组件需要从另一个组件中获得的东西。
感谢详细解释,非常有用。
+7

总的来说,您不经常使用协议、记录和自定义类型。对于那些大多应用于特定应用域的逻辑,使用传统的映射和常规函数即可。尽管如此,我会尽量解释何时需要使用它们。

总结:

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

如果您有一个在运行时调用不同事情的功能,并且它执行的操作必须针对它被调用的内容是特定的。那么,您想使用协议或多态方法。如果您可以根据第一个参数的类型来决定您要执行的操作,请使用协议;如果不可以,请使用多态方法。

如果您有一个根据 Map 类型执行不同事情的功能。例如,您可能需要一个标记 Map 代表某物,如 Person、Car、User 等。并且您有需要用不同类型的 Map 来调用的功能,并希望它们基于类型选择要执行的操作,则请使用 defrecord。

如果您有一个需要根据其调用的参数数量执行不同事情的功能,请使用多参数或变参。

最后,如果您发现自己正在编写许多以相同名称开始但以某种与您调用它的类型相关的区分符结束的功能,例如:`add-user`、`add-role`、`add-item`,这可能表明您可以使用协议或多态方法和单个 `add` 函数来对此进行建模。

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

今天刚买了这本书。确实值得一读,谢谢建议。
...