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

欢迎!请查阅关于页面了解一些有关该如何工作的更多信息。

+11

我在一个侧重于从多个来源和格式(xlsx、csv、jsons 等)进行数据整合的项目上工作。在实现第一个整合及其通过 spec 实现的验证后,我开始学习如何通过使用 protocolsdefrecords 来改进我的代码。

然而,protocols自定义类型 的用例对我来说并不明确。我应该何时使用 protocolsdefrecords?对此有何建议?

因为,正如我目前的学习,面向抽象的编程可以提高代码重用性。但是如何抓住这些瞬间呢?

我总是发现关于如何实现或与一些非常简单的与 Java 接口比较的教程,但关于如何更好地使用 protocolsrecords 来解决应用中更高级模式的材料却不多。

3 个答案

+12

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

通常,当你感觉到有个行为抽象,尤其是那些以后可能需要扩展的抽象(虽不是必须的),那么你应该在考虑协议和多态方法。这些工具在几个方面有所不同——协议在第一个参数上执行基于类型的快速分发,并支持一系列操作。多态方法在单个操作的所有参数上进行基于值的分发(这包括了在第一个参数上基于类型的分发)。当两者都可行时,通常协议更快、更好。

协议的一个建议——协议函数作为SPI(服务提供者接口)来连接实现,比作为API的函数消费者直接调用要好。将协议方法包裹在一个可以提供额外逻辑(如果需要的话)的正常函数中通常很有帮助。我们的经验表明,这在架构上以及代码随时间演化过程中都非常有效。一个缺点是它削弱了协议相对于多态方法的性能优势,所以请慎重考虑。

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

它们在构建上有所不同(记录有预构建的工厂方法,而映射则没有),并且具有一个“类型”(生成的具体记录类),这使得它们能够被协议挂钩。此外,将协议实现内联到记录中,为特定情况下的信息映射(利用JVM对这些路径的高度优化)提供了性能上的优势。记录与映射的选择并不简单——首先比较所有方面很重要。

by
由于SPI提到了很多缩写,在这个上下文中澄清这里所说的SPI的意思可能是个好主意。
by
是的,谢谢。我的意思是“服务提供者接口”,即你的组件需要从另一个组件获得的东西。
感谢详细的解释,非常有帮助。
+7

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

TL;DR

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

如果您有一个在运行时用不同内容调用的函数,其执行的内容必须特定于它被调用的内容。那么您应该使用协议或多方法。如果您可以根据第一个参数的类型选择要执行的操作,则使用协议;否则,使用多方法。

如果您有一个需要根据 Map 类型执行不同操作的函数。例如,您需要将 Map 标记为表示某物,如 Person、Car、User 等。您有将多个类型的 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、prev等操作。因此,deftype对于将任何形式的数据容器添加到语言中都是有用的,你几乎永远不会自己这样做。

defrecord

当需要创建自定义类型,为了给某些Map添加语义意义时,你会使用defrecord。

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

这只有在你需要代码在运行时根据类型执行不同操作时才有用。例如,有一条代码将接收不同类型的Map,并希望它为每种类型执行不同的操作,同时希望获得更好的性能。

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

defrecord只能允许根据函数第一个参数的类型执行不同的操作,记录的结构和值不能决定其类型。这通常被称为命名类型。你的记录之所以是某个类型,是因为你明确地那样命名。另一方面,defmulti允许鸭式类型,即一个Map之所以是某个类型,是因为它的内在结构及其/所包含的值。

defprotocol

当你想创建基于其第一个参数的本地类型的函数时,你会使用defprotocol。例如,一个将调用不同记录并应根据每个不同的记录类型执行不同操作的函数。

defmulti

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

多参函数

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

by
太棒了。非常有价值,我发现很少看到能传递**隐性知识**(正如在《Clojure元素》一书的引言中所描述的那样)的帖子。无法与经验丰富的程序员合作,这使得学习这种过程更加困难。

感谢您的贡献
+1 投票

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

今天刚买了这本书。确实是一本好书,谢谢建议。
...