请分享您的想法,参与2024 Clojure 状态调查!

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

+11投票
协议
编辑

我在一个项目中工作,这个项目主要关注从多个来源和格式(xlsx、csv、jsons 等)整合数据。在实现了第一个集成和利用 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标记为表示某人、一辆汽车、一个用户等。您有多个类型的Map会被调用,并希望它们根据该类型选择要执行的操作,则请使用defrecord。

如果您有一个根据被调用时传入的参数数量需要执行不同操作的函数,则请使用多参数。

最后,如果您发现自己在编写许多以相同名称开始但以某种与类型相关的区分符结尾的函数,比如add-useradd-roleadd-item,这可能是使用协议或多项方法并使用单个add函数来建模的良好指标。

deftype

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

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

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

为了更好地理解,deftype 允许数据封装。通常情况下,在 Clojure 中不需要数据封装,因为默认的数据结构和绑定都是不可变的,所以将数据暴露给外部作为只读不会存在危险。但就实现数据容器本身而言,如不可变数据结构或各种引用类型(如 Atom),你需要通过变异来提供空间和时间的有效实现。在这种情况下,你不应该让外部人员自由地访问数据,因为他们很容易破坏所需的性质。因此,你希望提供抽象接口,例如,对于双向链表,你可能会有 add-first(添加到头部)、add-last(添加到尾部)、remove(移除)、next(下一个)、previous(上一个)等。所以再次强调,deftype 对向语言添加任何形式的数据容器都很有用,而你几乎不需要自己这样做。

defrecord

当你需要一个自定义类型来给 Map 增加语义意义时,你会选择使用 defrecord。实际上,record 只是一个 Map,但它将类型替换为 record 类型名称。因此,它将不是 Map 类型类型,而是记录名称的类型。

如果你想有一个类型为 Person 的 Map,你会使用 defrecord。

这只有在需要根据类型在运行时执行不同的操作时才真正有用。也就是说,如果有一段代码将接收到不同类型的 Maps,并且你希望它针对每种类型执行不同的操作,并且你还希望有更好的性能。

我之所以这样说,是因为实际上有几种方式可以提供这种动态功能。一种是通过使用原生类型,并使用记录和协议提供。另一种是通过手动构建类型和使用 defmulti。后者更强大,因为你可以将类型表示为任何你想要的东西,并且类型甚至可以从中或数据的值本身推断出来。另一方面,它将更慢。

defrecord 只允许函数根据其第一个参数的类型 differently 处理,并且记录的结构和值不能决定其类型。这通常被称为名义类型。你的记录是某种类型,因为你明确地这样命名。另一方面,defmulti 允许使用 duck 类型,即一个 Map 可以根据其内在结构及其包含的值具有某种类型。

defprotocol

当你想要执行不同操作的功能时,这些操作的第一个参数基于其原生类型。例如,一个会以不同的记录调用,并且针对每种不同的记录类型执行不同操作的功能。

defmulti

就像我在简要介绍记录时解释的那样。如果你想让函数根据第一个参数的类型执行不同的操作,而不是因为其他参数的类型,或者因为任意/所有参数中的数据结构,或者因为任意/所有参数的值,你应该使用 defmulti 而不是 defprotocol。

多参数

另外,如果你只希望根据函数调用的参数数量以不同方式执行函数,你可以使用多参数函数。

by
太惊人了。非常有价值,我发现很少能传递 **默示知识** (正如 Zachary Tellman 在 *Clojure 元素* 介绍中所描述的)。无法与经验丰富的程序员合作会使学习这些过程变得非常困难。

感谢您的贡献
+1

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

今天刚刚买这本书。确实非常值得一读,谢谢推荐
...