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

欢迎!请查看关于页面以了解有关此操作的一些更多信息。

+11
协议
编辑过

我在一个项目中工作,这个项目专注于从多个来源和格式(xlsx、csv、json等)中集成数据。在实施第一个集成及其验证后,我开始研究如何通过使用protocolsdefrecords来改进我的代码。

但是,关于何时使用protocolscustom types并不清晰。我应该什么时候使用protocolsdefrecords?对此有一些推荐吗?

因为,当我学习时,面向抽象的编程会让代码的重用变得更好。但是如何找到这些时刻呢?

我总是发现有关如何实现或与Java接口进行一些非常浅显比较的教程,但关于如何利用protocolsrecords来解决应用程序中更大的模式,材料却不多。

3 个回答

+12

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

通常,你感到有行为抽象时,尤其是那个以后可能会用到扩展的场景(尽管这不是必需的),你应该在考虑协议和多态方法。这些工具在几个方面有所不同——协议执行快速的类型匹配分发,并支持操作组。多态方法对所有单个操作的参数进行基于值的分发(这包括在第一个参数上基于类型的分发)。在两者都可行的情况下,通常协议更快更好的。

一条协议建议——协议函数最好作为SPI来钩入实现,而不是作为API中的函数消费者直接调用。在对协议方法调用之前,用正常函数封装协议方法通常很有帮助,如果需要的话,可以为调用提供额外的逻辑。我们的经验表明,这在架构上以及随着时间的推移代码的演变中都非常好。一个缺点是它伤害了协议比多态方法的优势,因此请仔细考虑这一点。

记录通常可以与已知字段的信息用途相媲美,类似于映射。(deftype通常用于创建自己的自定义结构,通常是不同级别的用例。)与映射相比,有许多微妙的权衡。总的来说,消费者通常以相同的方式与映射和记录交互(其中记录实现了映射接口)。

它们在构建上有所不同(记录有预构建的工厂方法,映射没有),并且在有“类型”(生成的具体记录类),这使得它们可以由协议勾入。此外,在记录内部内联协议实现的能力,使得特定情况下信息映射与多态行为的结合点是一种完美的性能解决方案(通过利用JVM在此方面的优化路径)。记录与映射的选择不是简单的事情——首先比较所有维度很重要。

鉴于SPI涉及了这么多首字母缩略词,在这种情况下明确你所指的SPI可能会有所帮助。
是的,谢谢。我的意思是“服务提供程序接口”,基本上是你组件从另一个处需要的东西。
by
感谢详细的解释,非常有帮助。
+7
by Didibus

总的来说,你不太会经常使用协议、记录和自定义类型。使用普通的映射和常规函数就已经足以处理大多数应用程序领域的特定逻辑。尽管如此,我仍会尝试解释在何时需要使用它们。

TL;DR

在使用需要底层可变性的自定义数据容器时,或需要某种形式的数据封装时,请使用 deftype。这主要用于像数据结构或引用类型这样非常原始的结构。

如果你的函数在运行时要用不同的内容调用,且其行为必须针对调用它的内容具体化。那么你想使用协议或多方法。如果你能够根据第一个参数的类型选择要执行的操作,请使用协议;如果不能,请使用多方法。

如果你的函数需要根据映射的类型执行不同的操作。比如说你需要给映射标记为表示某物,如Person,Car,User等。并且你有需要用多种类型的映射调用的函数,你想让它们根据类型选择操作,那么请使用 defrecord。

如果你的函数需要根据被调用的参数个数执行不同的操作,请使用多arity。

最后,如果你发现自己写了大量以相同名称开头但以某种与调用它的类型相关的区分符结尾的函数。比如: add-useradd-roleadd-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等各种引用类型,你需要提供有效的空间和时间效率,这就要用到变异数据。在这种情况下,你不应该让外部使用者能够自由地操作数据,因为他们可能会轻易地破坏必需的不变性。因此,你希望提供一个抽象接口,例如对于双向链表,你可能会有add-first、add-last、remove、next、prev等方法。所以,再次强调,deftype对于将任何形式的数据容器添加到语言中非常有用,而这几乎是你自己几乎不会有的事情。

defrecord

当你需要创建一个自定义类型,以便给某个Map添加语义意义时,你会使用defrecord。其实record只是一个Map,只是将其类型替换为record类型名。所以,它不会有Map类型的类型,而是会有record名称作为类型。

假设你想要一个Person类型的Map,你会使用defrecord。

这种情况下的优点在于你需要一段代码在不同的运行时根据类型做不同的事情。例如,如果有代码将接收不同类型的Map,并且你想让它在每种特定类型上有所不同,同时希望获得更好的性能。

我在这里也提到这一点,因为实际上有很多方法可以提供这种动态功能。一种方法是用原生类型,由record和protocol提供。另一种是通过手动建模的类型和defmulti的使用。后者更强大,因为你可以将类型表示为你想要的任何东西,类型甚至可以从中自分数据结构或值的本身推断出来。另一方面,它会更慢。

defrecord只能允许函数根据其第一个参数的类型以及record的结构和值执行不同的操作。这通常被称作名义类型。你的record是指定类型的,因为你是显式命名它的。另一方面,defmulti支持鸭子类型,即Map可以根据其内在的结构和/或包含的值具有特定的类型。

defprotocol

当你想要函数根据其第一个参数的本地类型做不同的事情时,你会使用defprotocol。例如,一个将用不同的records调用的函数,并且应该对每种不同的record类型执行不同的操作。

defmulti

正如我简要讨论record时所解释的,如果你想让一个函数根据第一个参数的类型、其他参数的类型或数据的结构以及任何/所有参数的值做不同的事情,你就应该使用defmulti而不是defprotocol。

多参数

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

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

感谢您的贡献
+1
by

如果您想深入了解Clojure中的多态性,我推荐Paul Stadig的书 - Clojure Polymorphism。《Clojure Polymorphism》的链接为:[https://leanpub.com/clojurepolymorphism](https://leanpub.com/clojurepolymorphism)

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