“没有继承性的扩展,第 1 部分” 主要讨论了 Goovy、">Scala 和 Clojure 中为现有类添加新方法的机制,这也是 Java 下一代语言实现无继承扩展的方法之一。本文将探讨 Clojure 的协议如何以创新的方法拓展 Java 扩展功能,为表达式问题提供出色的解决方案。
尽管这期文章主要关注可扩展性,但也会略为涉及一些允许 Clojure 和 Java 代码无缝互操作的 Clojure 特性。这两种语言有着根本性的差别(Java 是命令式、面向对象的;而 Clojure 是函数式的),但 Clojure 实现了一些便捷的特性,使 Clojure 能够在确保最小摩擦的前提下处理 Java 结构。
Clojure 协议回顾
协议是 Clojure 生态系统的重要组成部分。上一期文章 展示了如何使用协议向现有类添加方法。协议也能帮助 Clojure 模拟面向对象的语言的为人熟知的许多特性。例如,Clojure 可模拟面向对象的类 — 数据与方法的组合,方法是通过协议将记录 与函数 绑定在一起的。为了理解协议与记录之间的交互,首先必须介绍映射,这是作为 Clojure 中记录基础的核心数据结构。
映射与记录
在 Clojure 中,映射就是一组名称-值对的集合(其他语言中常见的概念)。例如,清单 1 中的 “读取-求值-打印” 循环 (REPL) 的第一步就是创建一个包含有关 Clojure 编程语言信息的映射:
清单 1. 与 Clojure 映射交互
user=> (def language {:name "Clojure" :designer "Hickey" })#'user/languageuser=> (get language :name)"Clojure"user=> (:name language)"Clojure"user=> (:designer language)"Hickey"
Clojure 广泛使用映射,因此其中包含特殊的语法糖,可简化与映射的交互。为检索与某个键有关的值,您可以使用熟悉的 (get ) 函数。但 Clojure 会尽可能地简化此类常用操作。
在 Java 环境中,语言的源代码并非原生数据结构,必须对它进行分析和转换。在 Clojure(和其他 Lisp 变体)中,源代码表示属于 原生数据结构,比如列表,列表有助于解释语言中的奇怪语法。在 Lisp 解释器将列表作为源代码读取时,它会尝试着将列表的第一个元素解释为某些可调用 的元素,比如函数。因此在 清单 1 中,(:name language) 表达式将返回与 (get language :name) 表达式相同的结果。Clojure 之所以提供这种语法糖,是因为从映射中检索项目属于常用操作。
此外,在 Clojure 中,某些结构可放在函数调用插槽中,这扩展了可调用性(像调用函数一样调用这些结构的能力)。Java 程序只可以调用方法和内置语言语句。清单 1 展示了映射键(如 (:name language))在 Clojure 中可作为函数加以调用。映射本身也是可调用的;如果您认为替代语法 (language :name) 更容易阅读,也可以使用这种替代语法。Clojure 丰富的可调用图表使得这种语言更易于使用,从而减少了重复的语法(例如 Java 程序中常见的 get 和 set )。
然而,映射并不能完全模拟 JVM 类。Clojure 提供了其他方法来帮助您建模包括数据和行为在内的问题,更加无缝地集成底层 JVM。您可以创建对应于类似的底层 JVM 类且完整性各有不同的多种结构,包括类型 和记录 在内。您可以使用 (deftype ) 创建一个类型,通常用该类型来建模机械 结构。例如,如果您需要一个数据类型来持有 XML,那么很有可能会使用 (deftype MyXMLStructure) 表示 XML 内嵌的数据提取结构。在 Clojure 中,习惯于使用记录获得数据,信息记录 是应用程序的核心。为支持这种用法,Clojure 将在包含可调用性等特性的底层记录定义中自动包含大量接口。清单 2 中的 REPL 交互演示了记录的底层类和超类:
清单 2. 记录的底层类和超类
user=> (defrecord Person [name age postal])user.Personuser=> (def bob (Person."Bob" 42 60601))#'user/bobuser=> (:name bob)"Bob"user=> (class bob)user.Personuser=> (supers (class bob))#{java.io.Serializable clojure.lang.Counted java.lang.Object clojure.lang.IKeywordLookup clojure.lang.IPersistentMap clojure.lang.Associative clojure.lang.IMeta clojure.lang.IPersistentCollection java.util.Map clojure.lang.IRecord clojure.lang.IObj java.lang.Iterable clojure.lang.Seqable clojure.lang.ILookup}
在 清单 2 中,我创建了一个名为 Person 的新记录,它包含用于 name、age 和 postal 代码的字段。我可以使用 Clojure 针对构造函数调用的语法糖来构造此类新记录(使用类名称加一个句点作为函数调用)。返回值为带有名称空间的实例。(默认情况下,所有 REPL 交互都发生在 user 名称空间内。)可调用性规则仍然存在,因此我可以使用 清单 1 展示的语法糖来访问记录的成员。
调用 (class ) 函数时,它将返回 Clojure 创建的名称空间和类名(可与 Java 代码交互)。我还可以使用 (supers ) 来访问 Person 的超 class。在 清单 2 的最后四行中,Clojure 实现了几个接口,包括 IPersistentMap 等可伸缩性接口,该接口允许使用 Clojure 的原生映射语法来处理类和对象。自动包含的一组接口是记录与类型之间的一个重要差别,类型不包含任何自动接口实现。
使用记录实现协议
Clojure 协议就是指定函数及其签名的指定集合。清单 3 中的定义将创建一个协议对象和一组多态协议函数:
清单 3. Clojure 协议
(defprotocol AProtocol "A doc string for AProtocol abstraction" (bar [this a] "optional doc string for aar function") (baz [this a] [this a b] "optional doc string for multiple-arity baz function"))
清单 3 中的函数对一个参数的类型进行分派,这使得它在该类型上具有多态性(此类型通常被命名为 this,以模拟 Java 上下文占位符)。因此,所有协议函数至少必须有一个参数。通常,协议使用驼峰式大小写混合格式命名;因为它们将在 JVM 级别上具体化 Java 接口,因此与 Java 命名规范保持一致能够简化互操作性。
记录可以实现协议,就像是在 Java 语言中实现接口一样。记录必须(将在运行时检查)实现与协议签名匹配的函数。在清单 4 中,我创建了一个实现 AProtocol 的记录:
清单 4. 实现协议
(defrecord Foo [x y] AProtocol (bar [this a] (min a x y)) (baz [this a] (max a x y)) (baz [this a b] (max a b x y)));exercising the record(def f (Foo.1 200))(println (bar f 4))(println (baz f 12))(println (baz f 10 2000))
在 清单 4 中,我创建了一个名为 Foo 的记录,它带有两个字段:x 和 y。为了实现协议,我必须包含匹配其签名的函数。实现协议后,我可以为对象的实例调用函数,就像调用普通函数一样。在函数定义中,我可以访问该记录的两个内部字段(x 和 y)以及函数参数。