Clojure

多重方法和層級

Clojure 避開傳統物件導向方法,為每個新情況建立新的資料類型,而是偏好於在少數類型上建立大量的函數庫。然而,Clojure 完全認知到執行時期多態在啟用彈性且可擴充的系統架構上的價值。Clojure 透過多重方法系統支援精密的執行時期多態,該系統支援根據類型、值、屬性和元資料,以及一個或多個引數之間的關係進行分派。

Clojure 多重方法是分派函數和一個或多個方法的組合。當使用 defmulti 定義多重方法時,必須提供分派函數。此函數將應用於多重方法的參數,以產生分派值。然後,多重方法將嘗試尋找與分派值或從中衍生分派值的值關聯的方法。如果已定義(透過 defmethod),則會使用參數呼叫它,而這將會是多重方法呼叫的值。如果沒有與分派值關聯的方法,多重方法將尋找與預設分派值(預設為 :default)關聯的方法,並在存在時使用它。否則,呼叫會產生錯誤。

多重方法系統公開此 API:defmulti 建立新的多重方法,defmethod 建立並安裝與分派值關聯的多重方法的新方法,remove-method 移除與分派值關聯的方法,而 prefer-method 在方法之間建立順序,當它們在其他情況下會產生歧義時。

衍生由 Java 繼承(對於類別值)或使用 Clojure 的特殊層級系統組合決定。層級系統支援名稱(符號或關鍵字)之間的衍生關係,以及類別和名稱之間的關係。derive 函數建立這些關係,而 isa? 函數測試它們的存在。請注意,isa? 不是 instance?

您可以使用 (derive child parent) 定義層級關係。子項和父項可以是符號或關鍵字,且必須具有命名空間限定。

請注意 :: 讀取語法,:: 關鍵字解析命名空間。

::rect
-> :user/rect

derive 是基本的關係建立者

(derive ::rect ::shape)
(derive ::square ::rect)

parents / ancestors / descendantsisa? 讓您可以查詢層級

(parents ::rect)
-> #{:user/shape}

(ancestors ::square)
-> #{:user/rect :user/shape}

(descendants ::shape)
-> #{:user/rect :user/square}

(= x y) 暗示 (isa? x y)

(isa? 42 42)
-> true

isa? 使用層級系統

(isa? ::square ::shape)
-> true

您也可以使用類別作為子項(但不能作為父項,讓某個項目成為類別的子項的唯一方法是透過 Java 繼承)。

這允許您將新的分類法疊加在現有的 Java 類別層級上

(derive java.util.Map ::collection)
(derive java.util.Collection ::collection)

(isa? java.util.HashMap ::collection)
-> true

isa? 也測試類別關係

(isa? String Object)
-> true

就像 parents / ancestors 一樣(但 descendants 除外,因為類別後代是一個開放的集合)。

(ancestors java.util.ArrayList)
-> #{java.lang.Cloneable java.lang.Object java.util.List
    java.util.Collection java.io.Serializable
    java.util.AbstractCollection
    java.util.RandomAccess java.util.AbstractList}

isa? 會呼叫其對應元素上的 isa? 來處理向量

(isa? [::square ::rect] [::shape ::shape])
-> true

基於 isa? 的分派

多重方法在測試分派值比對時,會使用 isa? 而不是 =。請注意,isa? 的第一個測試是 =,因此精確比對有效。

(defmulti foo class)
(defmethod foo ::collection [c] :a-collection)
(defmethod foo String [s] :a-string)

(foo [])
:a-collection

(foo (java.util.HashMap.))
:a-collection

(foo "bar")
:a-string

prefer-method 用於在沒有任何一個方法佔優的情況下,消除多重比對的歧義。您只需針對每個多重方法宣告,一個分派值優先於另一個分派值

(derive ::rect ::shape)

(defmulti bar (fn [x y] [x y]))
(defmethod bar [::rect ::shape] [x y] :rect-shape)
(defmethod bar [::shape ::rect] [x y] :shape-rect)

(bar ::rect ::rect)
-> Execution error (IllegalArgumentException) at user/eval152 (REPL:1).
   Multiple methods in multimethod 'bar' match dispatch value:
   [:user/rect :user/rect] -> [:user/shape :user/rect]
   and [:user/rect :user/shape], and neither is preferred

(prefer-method bar [::rect ::shape] [::shape ::rect])
(bar ::rect ::rect)
-> :rect-shape

以上所有範例都使用多重方法系統所使用的全域階層,但也可以使用 make-hierarchy 建立完全獨立的階層,而且以上所有函數都可以將一個選用的階層當作第一個引數。

這個簡單的系統極為強大。了解 Clojure 多重方法與傳統 Java 式單一分派的關係的方法之一,是將單一分派視為一個多重方法,其分派函數會對第一個引數呼叫 getClass,而其方法會與那些類別關聯。Clojure 多重方法並未硬性綁定至類別/型別,它們可以基於引數的任何屬性、多個引數,可以驗證引數並導向錯誤處理方法等。

注意:在此範例中,關鍵字 :Shape 被用作分派函數,因為關鍵字是映射的函數,如 資料結構 區段所述。

(defmulti area :Shape)
(defn rect [wd ht] {:Shape :Rect :wd wd :ht ht})
(defn circle [radius] {:Shape :Circle :radius radius})
(defmethod area :Rect [r]
    (* (:wd r) (:ht r)))
(defmethod area :Circle [c]
    (* (. Math PI) (* (:radius c) (:radius c))))
(defmethod area :default [x] :oops)
(def r (rect 4 13))
(def c (circle 12))
(area r)
-> 52
(area c)
-> 452.3893421169302
(area {})
-> :oops