程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java 下一代: 沒有繼承性的擴展(二)探索 Clojure 協議

Java 下一代: 沒有繼承性的擴展(二)探索 Clojure 協議

編輯:關於JAVA

“沒有繼承性的擴展,第 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 展示的語法糖來訪問記錄的成員。

調用 (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)以及函數參數。

協議擴展選項

作為一種輕松擴展現有類和層次結構的方法,協議在設計時便考慮到了表達式問題。(有關表達式文檔的完整介紹,請參閱 上一期文章。)由於這些擴展是函數(就像 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)))

清單 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),總結無繼承擴展 系列。  

本欄目

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved