总的来说,您不太会经常使用协议、记录和自定义类型。对于大多数应用领域的特定逻辑,使用正常的映射和常规函数就足够了。不过,我仍会尝试解释何时需要它们。
TL;DR
当您需要实现自定义数据容器,并在内部需要可变性或某种形式的数据封装时,请使用 deftype。这主要用于非常原始的结构,如数据结构或引用类型。
如果有函数在不同的运行时调用时使用不同的参数,并且它所执行的操作必须针对其被调用的特定对象。那么您想要使用协议或多方法。如果可以通过选择操作来处理第一个参数的类型,则使用协议;否则,使用多方法。
如果有函数需要根据 Map 的类型执行不同的操作。比如说你需要给 Map 标记为代表某物,比如 Person、Car、User 等。而且有多个类型的函数将要被调用,而你希望它们根据类型来选择要做什么,那么请使用 defrecord。
如果有函数需要根据其被调用的参数数量执行某个操作,请使用多参数函数。
最后,如果您发现自己在编写许多具有相同名称但以某种与您调用的类型相关判别符结束的函数。比如 add-user
、add-role
、add-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。
多参数函数
此外,如果你想根据函数被调用的参数数量执行不同的操作,你可以使用多参数函数。