Clojure

協定

動機

Clojure 以抽象概念撰寫。有序列、集合、可呼叫性等抽象概念。此外,Clojure 提供許多這些抽象概念的實作。抽象概念由主機介面指定,實作則由主機類別指定。儘管這對於引導語言已足夠,但 Clojure 沒有類似的抽象概念和低階實作設施。協定資料類型功能新增強大且彈性的機制,用於抽象概念和資料結構定義,且不會與主機平台的設施相衝突。

協定有幾個動機

  • 提供高效能、動態多型建構,作為介面的替代方案

  • 支援介面中最好的部分

    • 僅限於規格,沒有實作

    • 單一類型可以實作多個協定

  • 同時避免一些缺點

    • 實作哪些介面是由類型作者在設計時選擇,之後無法延伸(儘管介面注入最終可能會解決此問題)

    • 實作介面會建立 isa/instanceof 類型關係和層級結構

  • 避免「表達式問題」,允許不同方獨立延伸類型、協定和協定在類型上的實作

    • 在沒有包裝器/轉接器的情況下執行此操作

  • 支援多重方法的 90% 案例(單一類型調度),同時提供更高階的抽象概念/組織

協定在 Clojure 1.2 中引入。

基礎知識

協定是一組已命名的方法和其簽章,使用 defprotocol 定義

(defprotocol AProtocol
  "A doc string for AProtocol abstraction"
  (bar [a b] "bar docs")
  (baz [a] [a b] [a b c] "baz docs"))
  • 沒有提供實作

  • 文件可以為協定和函式指定

  • 上述會產生一組多型函式和一個協定物件

    • 所有都由包含定義的命名空間限定

  • 產生的函式會在其第一個引數的類型上進行調度,因此必須至少有一個引數

  • defprotocol 是動態的,不需要 AOT 編譯

defprotocol 會自動產生一個對應的介面,名稱與協定相同,例如,給定協定 my.ns/Protocol,介面為 my.ns.Protocol。介面會有對應協定函式的函式,而協定會自動與介面的實例搭配使用。

請注意,您不需要在 deftypedefrecordreify 中使用此介面,因為它們直接支援協定

(defprotocol P
  (foo [x])
  (bar-me [x] [x y]))

(deftype Foo [a b c]
  P
  (foo [x] a)
  (bar-me [x] b)
  (bar-me [x y] (+ c y)))

(bar-me (Foo. 1 2 3) 42)
= > 45

(foo
 (let [x 42]
   (reify P
     (foo [this] 17)
     (bar-me [this] x)
     (bar-me [this y] x))))

> 17

想要參與協定的 Java 客戶端可以透過實作協定產生的介面,以最有效率的方式執行此操作。

當您想要一個不受您控制的類別或類型參與協定時,可以使用 extend 建構提供協定的外部實作

(extend AType
  AProtocol
   {:foo an-existing-fn
    :bar (fn [a b] ...)
    :baz (fn ([a]...) ([a b] ...)...)}
  BProtocol
    {...}
...)

extend 會採用一個類型/類別(或介面,請見下方),一個或多個協定 + 函式映射(已評估)對。

  • 當提供 AType 作為第一個引數時,會延伸協定方法的多型性來呼叫提供的函式

  • 函式映射是將關鍵字化的函式名稱對應到一般函式的映射

    • 這有助於輕鬆重複使用現有的函式和映射,以利於程式碼重複使用/混合,而無需衍生或組合

  • 您可以在介面上實作協定

    • 這主要是為了促進與主機(例如 Java)的互通

    • 但會開啟實作的多重繼承

      • 因為一個類別可以從多個介面繼承,而這兩個介面都實作協定

      • 如果一個介面從另一個介面衍生,則使用較衍生的介面,否則將使用哪一個介面並未指定。

  • 實作函式可以假設第一個引數是 AType 的實例

  • 您可以在 nil 上實作協定

  • 若要定義協定的預設實作(除了 nil 之外),只需使用 Object

協定是完全具象化的,並透過 extends?extenderssatisfies? 支援反射功能。

  • 請注意方便巨集 extend-typeextend-protocol

  • 如果您要提供內聯外部定義,這些定義會比直接使用 extend 更方便

(extend-type MyType
  Countable
    (cnt [c] ...)
  Foo
    (bar [x y] ...)
    (baz ([x] ...) ([x y zs] ...)))

  ;expands into:

(extend MyType
  Countable
   {:cnt (fn [c] ...)}
  Foo
   {:baz (fn ([x] ...) ([x y zs] ...))
    :bar (fn [x y] ...)})

延伸指南

協定是一個開放系統,可延伸到任何類型。為了將衝突降到最低,請考慮這些指南

  • 如果您不擁有協定或目標類型,您應該只在應用程式(而非公開程式庫)程式碼中延伸,並預期可能會被任一擁有者中斷。

  • 如果您擁有協定,您可以提供一些基本版本作為套件的一部分,以供常見目標使用,但須遵守專制的性質。

  • 如果您要發布一個潛在目標的程式庫,您可以為它們提供常見協定的實作,但須遵守您正在專制的這個事實。您在延伸 Clojure 本身附帶的協定時應特別小心。

  • 如果您是程式庫開發人員,您不應該在您既不擁有協定也不擁有目標時延伸。

另請參閱此 郵件清單討論

透過元資料擴充

自 Clojure 1.10 起,協定可選擇透過每個值的元資料進行擴充

(defprotocol Component
  :extend-via-metadata true
  (start [component]))

當 :extend-via-metadata 為 true 時,值可透過新增元資料來擴充協定,其中金鑰為完全限定的協定函式符號,而值為函式實作。協定實作會先檢查直接定義 (defrecord、deftype、reify),再檢查元資料定義,最後檢查外部擴充 (extend、extend-type、extend-protocol)。

(def component (with-meta {:name "db"} {`start (constantly "started")}))
(start component)
;;=> "started"