Clojure

資料類型:deftype、defrecord 和 reify

動機

Clojure 是以抽象方式撰寫的。有序列、集合、可呼叫性等抽象。此外,Clojure 提供了許多這些抽象的實作。這些抽象由主機介面指定,實作則由主機類別指定。雖然這對於引導語言來說已經足夠,但 Clojure 卻沒有類似的抽象和低階實作工具。 協定資料類型 功能新增了強大且彈性的機制,可用於抽象和資料結構定義,且與主機平台的工具相比毫不遜色。

基礎

資料類型功能 - deftypedefrecordreify,提供定義抽象實作的機制,而在 reify 的情況下,則提供這些實作的實例。抽象本身是由 協定 或介面定義。資料類型提供一個主機類型(在 deftype 和 defrecord 的情況下命名,在 reify 的情況下為匿名),具有一些結構(在 deftype 和 defrecord 的情況下為明確欄位,在 reify 的情況下為隱含閉包),以及抽象方法的選用型內部實作。它們以相對乾淨的方式支援存取主機的最高效能原始表示和多型機制。請注意,它們不只是主機內括號的建構。它們只支援主機功能的限定子集,通常比主機本身更動態。目的是,除非互操作迫使人們超越其限定範圍,否則不必離開 Clojure 即可在平台上取得效能最高的資料結構。

deftype 和 defrecord

deftypedefrecord 會動態產生編譯的位元組碼,用於具有給定欄位集合的名稱類別,以及一個或多個協定和/或介面的方法(選用)。它們適合動態和互動式開發,不必進行 AOT 編譯,而且可以在單一工作階段中重新評估。它們類似於 defstruct,會產生具有命名欄位的資料結構,但與 defstruct 的不同之處在於

  • 它們會產生一個唯一的類別,其欄位與給定的名稱相符。

  • 與結構中編碼類型的慣例不同,產生的類別具有適當的類型

  • 因為它們會產生一個命名類別,所以它具有可存取的建構函式

  • 欄位可以具有類型提示,而且可以是原始的

    • 請注意,目前非原始類型的類型提示不會用於限制欄位類型或建構函式引數,但會用於最佳化其在類別方法中的使用

    • 正在規劃限制欄位類型和建構函式引數

  • deftype/defrecord 可以實作一個或多個協定和/或介面

  • deftype/defrecord 可以使用特殊讀取語法 #my.thing[1 2 3] 撰寫,其中

    • 向量形式中的每個元素都會傳遞給 deftype/defrecord 的建構函式,且未經評估

    • deftype/defrecord 名稱必須完全限定

    • 僅在 Clojure 1.3 之後版本中提供

  • 在定義 deftype/defrecord Foo 時,會定義一個對應函式 ->Foo,將其引數傳遞給建構函式(僅限 1.3 及之後版本)

deftypedefrecord 在以下方面有所不同

  • deftype 除了建構函式外,不提供使用者未指定的任何功能

  • defrecord 提供持久性映射的完整實作,包括

    • 基於值的相等性和 hashCode

    • 元資料支援

    • 關聯式支援

    • 欄位的關鍵字存取器

    • 可延伸欄位(你可以關聯未提供給 defrecord 定義的鍵)

  • deftype 支援可變欄位,defrecord 則不支援

  • defrecord 支援額外的讀取器形式 #my.record{:a 1, :b 2},它會採用一個映射,根據下列規則初始化 defrecord

    • defrecord 名稱必須完全限定

    • 映射中的元素未經評估

    • 現有的 defrecord 欄位會採用鍵值

    • 在文字映射中沒有鍵值的 defrecord 欄位會初始化為 nil

    • 允許額外的鍵值,並將其新增至 defrecord

    • 僅在 Clojure 1.3 之後版本中提供

  • 在定義 defrecord Bar 時,會定義一個對應函式 map->Bar,它會採用一個映射,並使用其內容初始化新的記錄實例(僅限 1.3 及之後版本)

為什麼同時有 deftype 和 defrecord?

最後,大多數物件導向程式中的類別會分成兩個不同的類別:實作/程式設計領域的類別,例如字串或集合類別,或是 Clojure 的參考類型;以及代表應用程式領域資訊的類別,例如員工、採購單等。一直以來,使用類別來表示應用程式領域資訊的特性很不幸,因為它會導致資訊隱藏在特定類別的微語言之後,例如,即使看似無害的 employee.getName() 也是資料的客製化介面。將資訊放入此類類別中是一個問題,就像每本書都用不同的語言寫成會是一個問題一樣。你無法再對資訊處理採取通用方法。這會導致過度具體化的爆炸,以及重複使用的缺乏。

這正是 Clojure 一直鼓勵將此類資訊放入映射中的原因,而這項建議不會隨著資料類型而改變。透過使用 defrecord,您可以取得可泛用處理的資訊,加上類型驅動多態性的額外好處,以及欄位的結構效率。另一方面,定義為向量等集合的資料類型沒有映射的預設實作是沒有意義的,因此 deftype 適合用來定義此類程式建構。

整體而言,記錄會比結構映射更適合所有資訊承載用途,您應該將此類結構映射移至 defrecord。不太可能有很多程式碼嘗試將結構映射用於程式建構,但如果有的話,您會發現 deftype 更適合。

AOT 編譯的 deftype/defrecord 可能適用於 gen-class 的部分使用案例,其限制並非禁止事項。在這些情況下,它們的效能會比 gen-class 更佳。

資料類型和協定具有觀點

雖然資料類型和協定與主機建構有明確定義的關係,而且是將 Clojure 功能公開給 Java 程式的絕佳方式,但它們並非主要是互通建構。也就是說,它們不會嘗試完全模仿或適應主機的所有 OO 機制。特別是,它們反映以下觀點

  • 具體衍生很糟糕

    • 您無法從具體類別衍生資料類型,只能從介面衍生

  • 您應該始終針對協定或介面進行程式設計

    • 資料類型不能公開其協定或介面中沒有的方法

  • 不可變性應該是預設

    • 而且是記錄的唯一選項

  • 資訊封裝是愚蠢的

    • 欄位是公開的,使用協定/介面來避免依賴關係

  • 將多態性與繼承綁在一起很糟糕

    • 協定讓您擺脫它

如果您使用資料類型和協定,您將擁有乾淨的、基於介面的 API,供您的 Java 使用者使用。如果您正在處理一個乾淨的、基於介面的 Java API,資料類型和協定可用於與其互通並延伸它。如果您有一個「不良」的 Java API,您將必須使用 gen-class。只有這樣,您用來設計和實作 Clojure 程式的程式建構才能免於 OO 的附帶複雜性。

reify

雖然 deftype 和 defrecord 定義了命名類型,reify 定義了匿名類型並建立該類型的實例。使用案例是您需要一次實作一個或多個協定或介面,並希望利用當地內容。在這方面,它的使用案例類似於 Java 中的代理或匿名內部類別。

reify 的方法主體是詞彙閉包,可以參考周圍的當地範圍。reifyproxy 的不同之處在於

  • 僅支援協定或介面,沒有具體的超類別。

  • 方法主體是結果類別的真實方法,而不是外部函數。

  • 呼叫實例上的方法是直接的,不使用映射查詢。

  • 不支援動態交換方法映射中的方法。

結果比代理程式有更好的效能,無論是在建構或呼叫方面。在所有限制不具約束力的情況下,reify 優於代理程式。

Java 註解支援

使用 deftype、defrecord 和 definterface 建立的類型,可以發出包含用於 Java 互通的 Java 註解的類別。註解在 meta 中描述為

  • 類型名稱 (deftype/record/interface) - 類別註解

  • 欄位名稱 (deftype/record) - 欄位註解

  • 方法名稱 (deftype/record) - 方法註解

範例

(import [java.lang.annotation Retention RetentionPolicy Target ElementType]
        [javax.xml.ws WebServiceRef WebServiceRefs])

(definterface Foo (foo []))

;; annotation on type
(deftype ^{Deprecated true
           Retention RetentionPolicy/RUNTIME
           javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
           javax.xml.ws.soap.Addressing {:enabled false :required true}
           WebServiceRefs [(WebServiceRef {:name "fred" :type String})
                           (WebServiceRef {:name "ethel" :mappedName "lucy"})]}
  Bar [^int a
       ;; on field
       ^{:tag int
         Deprecated true
         Retention RetentionPolicy/RUNTIME
         javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
         javax.xml.ws.soap.Addressing {:enabled false :required true}
         WebServiceRefs [(WebServiceRef {:name "fred" :type String})
                         (WebServiceRef {:name "ethel" :mappedName "lucy"})]}
       b]
  ;; on method
  Foo (^{Deprecated true
         Retention RetentionPolicy/RUNTIME
         javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
         javax.xml.ws.soap.Addressing {:enabled false :required true}
         WebServiceRefs [(WebServiceRef {:name "fred" :type String})
                         (WebServiceRef {:name "ethel" :mappedName "lucy"})]}
       foo [this] 42))

(seq (.getAnnotations Bar))
(seq (.getAnnotations (.getField Bar "b")))
(seq (.getAnnotations (.getMethod Bar "foo" nil)))