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

欢迎!请参阅关于页面,了解该系统的更多信息。

+11
协议
编辑

我在一个项目中工作,这个项目侧重于从多个来源和格式(xlsx、csv、jsons等)集合同步化数据。在实现了第一个同步化并通过spec进行验证后,我开始研究如何通过使用protocotsdefrecords来改进我的代码。

然而,对于protocolcustom types的使用场景对我来说并不明确。我应该何时使用protocoldefrecords?对此有一些推荐吗?

因为,正如我所学习的那样,向抽象编程会让代码重用性更好。但如何捕捉这些时刻呢?

我发现关于如何实现教程或者与Java接口比较很浅的比较相当多,但关于这些更大的模式(可能是使用protocolrecords来更好地解决的应用程序)的材料却不多。

3 个答案

+12

协议(和多态方法)是Clojure的工具,提供了多态性、开放性和行为抽象。也就是说,调用者可以在不同的输入上调用相同的操作,并且会为它们选择并调用相应的实现。因为这些系统是开放的,可以在事后扩展,而无需修改调用者。

一般来说,每当感觉到行为抽象,尤其是将来可能需要扩展的行为抽象(尽管这不强制要求),你应该考虑使用协议和多态方法。这些工具有几个方面的区别——协议在第一个参数上执行快速的类型分发,并支持一组操作。多态方法对所有参数的一次操作执行值分发(这包括在第一个参数上基于类型的分发)。在两种方法都可行的情况下,协议通常更快、更好。

关于协议的一些建议——将协议函数作为SPI(服务提供者接口)来挂钩实现比在API作为直接调用的函数消费者更好。通常,在调用到协议的过程中,使用一个正常函数包装协议方法,如果需要的话,可以提供额外的逻辑。我们的经验表明,这种方法在架构上非常出色,并且随着时间的推移代码也会随之演变。一个缺点是,这会损害协议与多态方法之间的性能优势,所以需要仔细考虑。

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

它们在构建上有所不同(记录有预构建的工厂方法,而映射没有),并且有一个“类型”(生成的具体记录类),这使得它们可以通过协议进行挂钩。另外,将协议实现内联在记录中,使信息映射与多态行为的特定情况(通过利用JVM为此提供的高度优化的路径)在性能上达到了一个甜蜜点。记录与映射的选择并不是简单的——需要首先比较各个方面。

by
由于SPI涉及到许多缩写,可能在这个上下文中明确你所说的SPI的含义是个好主意。
by
是的,谢谢。我的意思是“服务提供者接口”,即您的组件从另一个组件需要的一些东西。
by
感谢详细的解释,非常有用。
+7
by

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

TL;DR

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

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

如果有函数需要根据 Map 的类型执行不同的操作。比如说你需要给 Map 标记为代表某物,比如 Person、Car、User 等。而且有多个类型的函数将要被调用,而你希望它们根据类型来选择要做什么,那么请使用 defrecord。

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

最后,如果您发现自己在编写许多具有相同名称但以某种与您调用的类型相关判别符结束的函数。比如 add-useradd-roleadd-item,这可能是一个很好的指标,表明您可以用协议或多方法以及单个 add 函数来建模这个问题。

deftype

我将从 deftype 开始。您最少会用到 deftype。deftype 允许您实现一个新的抽象数据类型(ADT)。它在创建新的数据容器类型时非常有用。基本上,任何需要封装数据(通常是可变数据)并且只通过一个安全接口访问数据的东西,这个接口以对该类型有意义的方式强制执行所有数据不变性。

例如,如果您想添加一个新数据结构,比如您需要实现一个双链表,您会使用 deftype。

现在,在这种情况下基本上您很少需要它,因为大多数有用的数据容器已经由核心、Interop 或作为库实现了。例如,Java 已经提供了一个双链表: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 实现。

这仅在需要根据类型在运行时执行不同操作时才有用。例如,如果有一些代码将接收不同类型的 Map,并且你想根据类型以及性能不同处理每种类型。

我还提到了这一点,因为实际上有几种方法可以提供这种动态功能。一种是通过使用原生类型,这由记录和协议提供。另一种是手动定义类型,并使用 defmulti。后一种更强大,因为你可以将类型表示为你想要的任何东西,类型甚至可以从数据结构的结构或值中推断出来。然而,它可能会更慢。

defrecord 只允许函数根据其第一个参数的类型执行不同的事情,并且记录的结构和值不能决定其类型。这通常称为命名类型。您的记录具有某种类型,是因为您明确地这样命名。相比之下,defmulti 允许进行鸭子类型匹配,即一个 Map 可以因其内在结构以及/或包含的值具有某种类型。

defprotocol

当你想要函数根据其第一个参数的原生类型执行不同的操作时,你会选择 defprotocol。例如,一个可能会被不同记录调用并且为不同的记录类型做不同处理的函数。

defmulti

正如我在简要介绍 records 时所解释的那样。如果你想函数根据第一个参数的类型以外的类型、数据的结构,或任何参数的值进行不同的操作,你应该使用 defmulti 而不是 defprotocol。

多参数函数

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

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

感谢您的贡献
+1 投票

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

今天刚买这本书。的确是一本好书,感谢推荐
...