Java 下一代: 没有担任性的扩展(二)摸索 Clojure 协议
副标题#e#
“没有担任性的扩展,第 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/language user=> (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.Person user=> (def bob (Person."Bob" 42 60601)) #'user/bob user=> (:name bob) "Bob" user=> (class bob) user.Person user=> (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 展示的语法糖来会见记录的成员。
#p#分页标题#e#
挪用 (class ) 函数时,它将返回 Clojure 建设的名称空间和类名(可与 Java 代码交互)。我还可以利用 (supers ) 来会见 Person 的超 class。在 清单 2 的最后四行中,Clojure 实现了几个接口,包罗 IPersistentMap 等可伸缩性接口,该接口答允利用 Clojure 的原生映射语法来处理惩罚类和工具。自动包括的一组接口是记录与范例之间的一个重要不同,范例不包括任何自动接话柄现。
#p#副标题#e#
利用记录实现协议
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)以及函数参数。
协议扩展选项
作为一种轻松扩揭示有类和条理布局的要领,协议在设计时便思量到了表达式问题。(有关表达式文档的完整先容,请参阅 上一期文章。)由于这些扩展是函数(就像 Clojure 中的其他内容一样),因此不会呈现面向工具语言所固有的身份和担任问题。并且这种机制支持各类有用的扩展。
Clojure 是一种托管式语言:它被设计为(利用协议)在多种平台上运行,包罗 .NET 和 JavaScript(通过 ClojureScript 编译器实现)。JavaScript 需要一种可以或许配置、卸除、加载和评估代码的情况。因此 ClojureScript 界说了 BrowserEnv 记录,用它为得当的 JavaScript 情况(欣赏器、REPL 或伪情况)处理惩罚生命周期函数,譬喻 setup 和 teardown。清单 5 给出了 BrowserEnv 的记录界说:
清单 5. ClojureScript 的 BrowserEnv 记录
(defrecord BrowserEnv [] repl/IJavaScriptEnv (-setup [this] (do (require 'cljs.repl.reflect) (repl/analyze-source (:src this)) (comp/with-core-cljs (server/start this)))) (-evaluate [_ _ _ js] (browser-eval js)) (-load [this ns url] (load-javascript this ns url)) (-tear-down [_] (do (server/stop) (reset! server/state {}) (reset! browser-state {}))))
在 IJavaScriptEnv 协议中界说的生命周期要领支持实现措施(如欣赏器)会见通用接口。在函数名称开头处利用连字符(譬喻,(-tear-down ))是 ClojureScript(而非 Clojure)的类型。
表达式问题办理方案的另一个方针是可以或许为现有条理布局添加新特性,同时担保无需从头编译或 “触及” 现有条理布局。在版本 1.5 中,Clojure 引进了名为 Reducers 的高级荟萃库。这个库添加了合用于多种荟萃范例的自动并发处理惩罚。为了操作 Reducers 库,现有范例必需实现该库的一个要领,即 coll-fold。由于回收了协议和便捷的 extend-protocol 宏(该宏答允您一次性将一个协议扩展到多种范例),(coll-fold ) 函数可跨多种焦点范例举办利用,如清单 6 所示:
清单 6. Reducers 将 (coll-fold ) 毗连到多种范例
(extend-protocol CollFold nil (coll-fold [coll n combinef reducef] (combinef)) Object (coll-fold [coll n combinef reducef] ;;can't fold, single reduce (reduce reducef (combinef) coll)) clojure.lang.IPersistentVector (coll-fold [v n combinef reducef] (foldvec v n combinef reducef)) clojure.lang.PersistentHashMap (coll-fold [m n combinef reducef] (.fold m n combinef reducef fjinvoke fjtask fjfork fjjoin)))
#p#分页标题#e#
清单 6 中的 (extend-protocol ) 挪用将 CollFold 协议(个中只包括一个 (coll-fold )要领)毗连到 nil、Object、IPersistentVector 和 PersistentHashMap 范例。即便 nil(Clojure 中等同于 Java 语言 null 的变体)在这个库中也可以正常利用,处理惩罚空荟萃的常见边沿环境。Reducers 库还会毗连到两个焦点荟萃类,即 IPersistentVector 和 IPersistentHasMap,以便在这些荟萃条理布局的顶层四周添加 Reducer 成果。
Clojure 回收一组优雅的构建块支持便捷而强大的扩展。由于这种语言基于函数,而非基于类,所以部门开拓人员大概不习惯其代码组织方法 —— Clojure 未将类作为主要组织原则。Clojure 的代码组织方法与 Java 概略沟通,但内容比 Java 精简一些。Java 中有包、类和要领,而 Clojure 中有名称空间(大抵对应于包)和函数(大抵对应于要领)。Clojure 协议还会在须要时生成原生 Java 接口,以便开拓人员用它们实现互操纵性。在 Clojure 中,最便捷的成果是在组件界线界说协议,将雷同的函数和协议放在一个名称空间内。Clojure 不具备类这种信息埋没机制,但您可以界说名称空间私有函数(利用 (defn- ) 函数界说)。
Clojure 在名称空间中的代码组织使得整洁、居中的扩展成为大概。调查 清单 6 中的 CollFold 协议,它呈此刻 Clojure 源代码的 reducers.clj 文件中。此文件是在 Clojure 1.5 版本中添加的,协议、新范例和扩展均处于此文件中。操作协议扩展,您就可以再次操作焦点范例(譬喻 Object),并添加 Reducer 成果,部门此类成果是通过 reducers 名称空间内的名称空间私有函数来实现的。Clojure 以极高的准确度为现有条理布局添加了重要的新行为,并且不会提高巨大度,还能将所有相关细节生存在一个位置。
(extend-type ) 宏雷同于 (extend-protocol ) 宏;利用 (extend-type ) 宏,您可以同时为一个范例添加多个协议。清单 7 展示了 ClojureScript 如何向 arrays 添加荟萃成果:
清单 7. 向 JavaScript 数组添加荟萃成果
(extend-type array ICounted (-count [a] (alength a)) IReduce (-reduce [col f] (array-reduce col f)) (-reduce [col f start] (array-reduce col f start)))
在 清单 7 中,ClojureScript 需要 JavaScript 数组来响应 Clojure 函数,譬喻 (count ) 和 (reduce )。(extend-type ) 宏答允在一个位置上实现多种协议。Clojure 期望荟萃响应 count 而非 length,因此毗连了 ICounted 协议和函数,并添加了适当的要领别名。
协议的详细化不需要记录。就像 Java 中的匿名工具一样,协议也可以详细化并内联利用,如清单 8 所示:
清单 8. 协议的内联详细化
(let [z 42 p (reify AProtocol (bar [_ a] (min a z)) (baz [_ a] (max a z)))] (println (baz p 12)))
在 清单 8 中,我利用了一个 let 块来建设两个当地绑定:x 和 p,即内联协议界说。在建设匿名协议时,我仍然可以会见当地浸染域:个中利用 z 作为参数是正当的,因为 z 处于此 let 块的浸染域内。通过这种方法,详细化的协议可以像闭包块一样封装其情况。请留意,我并未完整实施协议;baz 函数的自变量版本并不完整。差异于 Java 接口,协议实现是可选的。假如 Clojure 需要的协议要领并不存在,它不会在编译时强制利用协议,而是生成一条运行时错误。
竣事语
本期的 Java 下一代 文章摸索了如何将 Java 中像类和接口这样的民众类型映射为 Clojure 中的布局。另外还摸索了 Clojure 中对协议的各类用法,以及 Clojure 如何轻松优雅地办理表达式问题,还先容了几种实际变体。在下一期文章中,我将摸索 Groovy 中的混入类 (mixin),总结无担任扩展 系列。
本栏目