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

欢迎!请访问关于页面了解更多关于这个网站如何运作的信息。

+11
协议
编辑

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

然而,关于何时使用protocolscustom types的使用场景对我来说并不明确。我应该何时使用protocolsdefrecords?有什么建议吗?

因为在学习过程中,根据抽象编程可以使代码重用变得更好。但如何找到这些时刻呢?

我总是能找到有关如何实现或与Java接口进行非常浅层次比较的教程,但没有太多关于在您的应用中可以使用protocolsrecords来更好解决问题的模式材料。

3 个答案

+12

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

通常,无论你感受到什么行为抽象,尤其是那些可以用于后期扩展的(尽管这不是必需的),你应该查看协议和多重方法。这些工具在几个方面有所不同——协议对第一个参数进行快速的基于类型的调度,并支持操作组。多重方法对所有单一操作的参数进行基于值的调度(这包括第一个参数的基于类型的调度)。在两者都可行的情况下,协议通常是更快和更好的。

关于协议的一些建议——协议函数更适合作为SPI来挂钩实现,而不是在API中直接作为函数消费者调用的函数。通常,用可以提供附加逻辑的正常函数包装协议方法是有帮助的,该附加逻辑在调用协议之前或者之后。我们的经验表明,这种架构在代码随时间演进时效果很好。一个缺点是这损害了协议相对于多重方法的性能优势,因此请仔细考虑。

记录最经常地与具有已知字段的映射相提并论,用于信息用途。(deftype通常用于创建自己的自定义结构,并且通常是不同的、更底层的用例。)在将它们与映射相提并论时,有许多细微的权衡。一般来说,消费者通常以相同的方式与映射和记录交互(其中记录实现了映射接口)。

它们在构建方面往往不同(记录具有预先构建的工厂方法,而映射没有),并且在拥有“类型”(生成的具体记录类)上有所不同,这使得它们可以通过协议来挂钩。此外,在记录内部内联协议实现的能力,为特定的信息映射案例(通过利用JVM中为此高度优化的路径)提供了性能上的最佳点。记录与映射不是简单的选择——在比较所有方面之前很重要。

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

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

TL;DR(太长不看,即总结)

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

如果您有一个在运行时以不同方式调用的函数,且其行为必须针对所调用的事物特定化。那么您想要使用协议或多方法。如果您可以根据第一个参数的类型选择要执行的操作,则使用协议;如果不能,则使用多方法。

如果您有一个根据 Map 的类型执行不同操作的函数。例如,您需要将 Map 标记为表示某物,如 Person、Car、User 等。您还有将调用带有多种类型 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,为了提供一个空间和时间效率高的实现,你需要进行突变。在这种情况下,你不应该让外人能够随意接触数据,因为他们很可能会破坏所需的 Doe。因此,你希望提供一个抽象接口,比如对于双链表,你可能会有 add-first(添加第一个元素)、add-last(添加最后一个元素)、remove(移除)、next(下一项)、prev(上一项)等。所以,deftype 对向语言添加任何形式的数据容器都很有用,而你几乎不需要自己这样做。

defrecord

当你需要创建一个自定义类型,以便为某些 Map 添加语义意义时,你会用 defrecord。

比如说你想要一个类型为 Person 的 Map,你会用 defrecord 来实现。

这只是在需要运行时根据类型执行不同操作的代码时才有用。比如,如果有一段代码会接收到不同类型的 Maps,并希望它为每种类型都执行不同的操作,并且希望有更好的性能。

我之所以说“也”,因为实际上有几种方法可以提供这种动态功能。一种可以是通过使用本地类型,record 和 protocols 提供了这一点。另一种是手动模拟类型和使用 defmulti。后者更强大,因为你可以将类型表示为任何你想要的东西,类型甚至可以从数据的结构或值中推断出来。另一方面,它可能会更慢。

defrecord 只允许函数根据它们的第一个参数的类型执行不同的操作,记录的结构和值不能决定它的类型。这通常被称为名义类型。你的记录是某种类型,因为你明确地这样命名了。另一方面,defmulti 允许进行“鸭子类型”实现,即一个 Map 可以是某种类型,因为它的内在结构以及它包含的值。

defprotocol

当你想要根据其第一个参数的本地类型执行不同操作的功能时,你会选择 defprotocol。比如说,一个将用不同记录调用的函数,针对不同的记录类型应该执行不同的操作。

defmulti

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

多元函数

此外,如果你只是想让函数根据它被调用的参数数量执行不同的操作,你只需使用多元函数。

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

感谢您的贡献
+1 投票

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

今天刚买了这本书。确实读后受益匪浅,感谢推荐
...