Clojure

数据类型:deftype、defrecord 和 reify

动机

Clojure 是用抽象来编写的。有序列、集合、可调用性等的抽象。此外,Clojure 还提供了许多这些抽象的实现。抽象由主机接口指定,实现由主机类指定。虽然这足以引导语言,但它使 Clojure 缺乏类似的抽象和底层实现设施。该 协议数据类型 功能增加了强大的灵活机制,用于抽象和数据结构定义,而不会对主机平台的设施做出任何妥协。

基础

数据类型功能 - deftypedefrecordreify ,提供了定义抽象实现的机制,在 reify 的情况下,还提供了这些实现的实例。抽象本身是由 协议 或接口定义的。数据类型提供主机类型(在 deftype 和 defrecord 中命名,在 reify 中匿名),具有一些结构(在 deftype 和 defrecord 中为显式字段,在 reify 中为隐式闭包),以及可选的抽象方法的类型内实现。它们以相对干净的方式支持访问主机的高性能原始表示和多态机制。注意,它们不仅仅是主机括号内的结构。它们只支持主机设施的有限子集,通常比主机本身更具动态性。目的是,除非互操作迫使人们超出其有限范围,否则人们不必离开 Clojure 来获得平台上可能获得的最高性能的数据结构。

deftype 和 defrecord

deftypedefrecord 动态地为具有给定字段集的命名类生成编译的字节码,以及可选地为一个或多个协议和/或接口生成方法。它们适合动态和交互式开发,不需要 AOT 编译,可以在单个会话中重新评估。它们类似于 defstruct 生成具有命名字段的数据结构,但与 defstruct 不同的是

  • 它们生成一个唯一的类,其字段对应于给定的名称。

  • 生成的类具有适当的类型,与在元数据中为结构编码类型的约定不同

  • 因为它们生成一个命名类,所以它有一个可访问的构造函数

  • 字段可以具有类型提示,并且可以是原始类型

    • 注意,当前非原始类型的类型提示不会用于约束字段类型或构造函数参数,但将用于优化其在类方法中的使用

    • 约束字段类型和构造函数参数正在计划中

  • deftype/defrecord 可以实现一个或多个协议和/或接口

  • deftype/defrecord 可以使用特殊的读取器语法 #my.thing[1 2 3] 编写,其中

    • 向量形式中的每个元素都会未评估地传递给 deftype/defrecord 的构造函数

    • deftype/defrecord 的名称必须是完全限定的

    • 仅在 1.3 及更高版本的 Clojure 中可用

  • 当定义 deftype/defrecord Foo 时,会定义一个相应的函数 ->Foo,该函数会将其参数传递给构造函数(仅 1.3 及更高版本)

deftypedefrecord 在以下方面有所不同

  • deftype 不提供用户未指定的功能,除了构造函数之外

  • defrecord 提供持久映射的完整实现,包括

    • 基于值的相等性和 hashCode

    • 元数据支持

    • 关联支持

    • 字段的关键字访问器

    • 可扩展字段(您可以 assoc 未在 defrecord 定义中提供的键)

    • 等等

  • deftype 支持可变字段,defrecord 不支持

  • defrecord 支持 #my.record{:a 1, :b 2} 的另一种读取器形式,它接受一个映射,该映射根据以下内容初始化 defrecord:

    • defrecord 的名称必须是完全限定的

    • 映射中的元素未评估

    • 现有的 defrecord 字段接受键控值

    • 映射中没有键控值的 defrecord 字段初始化为 nil

    • 允许额外的键控值并将其添加到 defrecord 中

    • 仅在 1.3 及更高版本的 Clojure 中可用

  • 当定义 defrecord Bar 时,会定义一个相应的函数 map->Bar,该函数接受一个映射并使用其内容初始化一个新的记录实例(仅 1.3 及更高版本)

为什么既有 deftype 又有 defrecord?

最终,大多数 OO 程序中的类分为两类:那些是实现/编程域的工件的类,例如 String 或集合类,或者 Clojure 的引用类型;以及代表应用程序域信息的类,例如 Employee、PurchaseOrder 等。一直以来,将类用于应用程序域信息的不幸特点是,它导致信息隐藏在特定于类的微语言后面,例如,即使看似无害的 employee.getName() 也是对数据的自定义接口。将信息放入这些类中是一个问题,就像让每本书都用不同的语言编写一样是一个问题。您不能再对信息处理采取通用的方法。这会导致不必要的特异性激增,以及可重用性匮乏。

这就是为什么 Clojure 一直鼓励将此类信息放入映射中,而且该建议在数据类型出现后并没有改变。通过使用 defrecord,您获得了可通用操作的信息,以及类型驱动的多态性的额外好处,以及字段的结构效率。另一方面,对于定义像向量这样的集合的数据类型来说,没有意义的是,它有一个默认的映射实现,因此 deftype 适合定义此类编程构造。

总的来说,记录在所有承载信息的用途方面都优于结构映射,您应该将此类结构映射迁移到 defrecord。不太可能会有太多代码尝试将结构映射用于编程构造,但如果有的话,您会发现 deftype 更适合。

AOT 编译的 deftype/defrecord 可能会适合 gen-class 的一些用例,在这些用例中,它们的局限性不会成为障碍。在这些情况下,它们将比 gen-class 具有更好的性能。

数据类型和协议是有观点的

虽然数据类型和协议与主机构造具有明确定义的关系,并且是将 Clojure 功能公开给 Java 程序的好方法,但它们并不是主要的面向互操作的构造。也就是说,它们没有试图完全模仿或适应主机的所有 OO 机制。特别是,它们反映了以下观点

  • 具体派生不好

    • 您不能从具体类派生数据类型,只能从接口派生

  • 您应该始终对协议或接口进行编程

    • 数据类型不能公开其协议或接口中没有的方法

  • 不可变性应该是默认选项

    • 而且是记录的唯一选项

  • 信息的封装是愚蠢的

    • 字段是公共的,使用协议/接口来避免依赖项

  • 将多态性与继承绑定在一起是不好的

    • 协议使您摆脱了这种束缚

如果您使用数据类型和协议,您将拥有一个干净的基于接口的 API,可以提供给您的 Java 消费者。如果您正在处理干净的基于接口的 Java API,则可以使用数据类型和协议与之交互并扩展它。如果您有一个“糟糕的”Java API,则您必须使用 gen-class。只有通过这种方式,您用来设计和实现 Clojure 程序的编程构造才能摆脱 OO 的偶然复杂性。

reify

虽然 deftype 和 defrecord 定义命名类型,但 reify 既定义匿名类型,又创建该类型的实例。用例是当您需要一个或多个协议或接口的一次性实现,并且希望利用本地上下文。在这方面,它的用例类似于 Java 中的代理或匿名内部类。

reify 的方法体是词法闭包,可以引用周围的本地范围。reifyproxy 不同,因为

  • 只支持协议或接口,不支持具体超类。

  • 方法体是生成的类的真实方法,而不是外部函数。

  • 对实例上的方法的调用是直接的,而不是使用映射查找。

  • 不支持方法映射中方法的动态交换。

结果是比 proxy 更好的性能,无论是在构建还是调用方面。在所有不禁止其约束的情况下,reify 优于 proxy。

Java 注解支持

使用 deftype、defrecord 和 definterface 创建的类型,可以发出包含 Java 注解的类,用于 Java 互操作。注解被描述为关于

  • 类型名称 (deftype/record/interface) - 类注解

  • 字段名称 (deftype/record) - 字段注解

  • 方法名称 (deftype/record) - 方法注解

示例

(import [java.lang.annotation Retention RetentionPolicy Target ElementType]
        [javax.xml.ws WebServiceRef WebServiceRefs])

(definterface Foo (foo []))

;; annotation on type
(deftype ^{Deprecated true
           Retention RetentionPolicy/RUNTIME
           javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
           javax.xml.ws.soap.Addressing {:enabled false :required true}
           WebServiceRefs [(WebServiceRef {:name "fred" :type String})
                           (WebServiceRef {:name "ethel" :mappedName "lucy"})]}
  Bar [^int a
       ;; on field
       ^{:tag int
         Deprecated true
         Retention RetentionPolicy/RUNTIME
         javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
         javax.xml.ws.soap.Addressing {:enabled false :required true}
         WebServiceRefs [(WebServiceRef {:name "fred" :type String})
                         (WebServiceRef {:name "ethel" :mappedName "lucy"})]}
       b]
  ;; on method
  Foo (^{Deprecated true
         Retention RetentionPolicy/RUNTIME
         javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
         javax.xml.ws.soap.Addressing {:enabled false :required true}
         WebServiceRefs [(WebServiceRef {:name "fred" :type String})
                         (WebServiceRef {:name "ethel" :mappedName "lucy"})]}
       foo [this] 42))

(seq (.getAnnotations Bar))
(seq (.getAnnotations (.getField Bar "b")))
(seq (.getAnnotations (.getMethod Bar "foo" nil)))