(defprotocol AProtocol
"A doc string for AProtocol abstraction"
(bar [a b] "bar docs")
(baz [a] [a b] [a b c] "baz docs"))
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。介面會有對應協定函式的函式,而協定會自動與介面的實例搭配使用。
(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?、extenders 和 satisfies? 支援反射功能。
請注意方便巨集 extend-type 和 extend-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"