承接上期探討Groovy、Scala、Clojure為既有類別添加方法的機制,本文聚焦Clojure協議如何以創新方式拓展Java擴展能力,優雅解決「表達式問題」。
協議是Clojure生態的關鍵組件,能模擬OO語言的「類別——資料與方法的結合」:透過協議將記錄與函數綁定。理解此機制,須先認識映射——記錄的基礎資料結構。
映射即鍵值對集合,Clojure提供豐富語法糖簡化操作:
user=> (def language {:name "Clojure" :designer "Hickey"})
user=> (:name language) ; 語法糖等同 (get language :name)
"Clojure"
映射鍵可作為函數調用((:name language)),映射本身亦可調用((language :name)),擴展了可調用性,減少Java常見的get/set樣板碼。
但映射無法完全模擬JVM類別。Clojure提供類型(deftype,建模機械結構)與記錄(defrecord,應用核心資料)。記錄自動實作多介面(IPersistentMap、java.io.Serializable等),支援原生映射語法操作物件:
user=> (defrecord Person [name age postal]) user=> (def bob (Person. "Bob" 42 60601)) user=> (:name bob) ; "Bob" user=> (supers (class bob)) ; 含IPersistentMap、ILookup、Iterable等
協議定義函數簽名集合,首參數(慣命名this)決定多態分派:
(defprotocol AProtocol (bar [this a]) (baz [this a] [this a b])) ; 多重arity (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)))
實作協議後,即可如同普通函數調用:(bar f 4)。函數體可存取記錄內部欄位(x、y)與參數。
協議設計初衷即解決表達式問題——「在不重編既有碼、不犧牲型別安全前提下,為資料類型添加新案例與新函數」。因擴展本質是函數,避開OO語言固有的身份與繼承糾葛。
ClojureScript定義BrowserEnv記錄實作IJavaScriptEnv協議,處理JavaScript環境生命週期(setup、teardown、evaluate、load),展現協議在跨平台抽象的威力。
Clojure 1.5引入Reducers庫,需現有類型實作coll-fold。透過extend-protocol宏一次擴展多類型:
(extend-protocol CollFold nil (coll-fold [coll n combinef reducef] (combinef)) Object (coll-fold [coll n combinef reducef] (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)))
連nil(相當Java null)也獲支援,優雅處理空集合邊界。核心集合IPersistentVector、PersistentHashMap獲並發摺疊能力,所有相關細節封裝在reducers.clj單一名稱空間。
ClojureScript為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)))
別名count對應length,滿足Clojure集合協議期望。
協議可無記錄直接具體化,如Java匿名類別:
(let [z 42
p (reify AProtocol
(bar [_ a] (min a z))
(baz [_ a] (max a z)))]
(println (baz p 12)))
let區塊建立區域綁定z,匿名協議捕獲環境變數,宛如閉包。協議實作可選——缺失方法僅拋運行時錯誤,不強制編譯期檢查。
Clojure以名稱空間(對應Java套件)與函數(對應方法)組織代碼。協議必要時生成原生Java介面供互操作。最佳實踐:在組件邊界定義協議,相關函數與協議歸屬同一名稱空間。無類別資訊隱藏,但可用defn-定義名稱空間私有函數。
CollFold協議定義於reducers.clj,新類型與擴展同檔案,相關細節集中管理,精準擴展核心類型且不增複雜度。
Clojure協議將Java介面/類別映射為函數式結構,以extend/extend-protocol/reify等機制優雅解決表達式問題。下一期將探討Groovy混入類,總結無繼承擴展系列。