Clojure

データ型: deftype、defrecord、reify

動機

Clojure は抽象化の観点から記述されています。シーケンス、コレクション、呼び出し可能性などの抽象化があります。さらに、Clojure はこれらの抽象化の多くの実装を提供します。抽象化はホストインターフェースによって指定され、実装はホストクラスによって指定されます。これは言語のブートストラップには十分でしたが、Clojure に同様の抽象化および低レベルの実装機能が残されていませんでした。プロトコルデータ型の機能は、ホストプラットフォームの機能との妥協なしに、抽象化とデータ構造定義のための強力で柔軟なメカニズムを追加します。

基本

データ型機能 - deftypedefrecordreify は、抽象化の実装を定義するメカニズムを提供し、reify の場合はそれらの実装のインスタンスを提供します。抽象化自体は、プロトコルまたはインターフェースによって定義されます。データ型は、ホストタイプ(deftype と defrecord の場合は名前付き、reify の場合は匿名)を提供し、いくつかの構造(deftype と defrecord の場合は明示的なフィールド、reify の場合は暗黙的なクロージャ)と、オプションで抽象メソッドの型内実装を提供します。それらは、比較的クリーンな方法で、ホストの最高パフォーマンスのプリミティブ表現とポリモーフィズムメカニズムへのアクセスをサポートします。注意: それらは単なるホストインパーレン構成ではありません。それらはホスト機能の限定されたサブセットのみをサポートし、多くの場合、ホスト自体よりも動的です。意図は、相互運用によって限定された範囲を超えることを余儀なくされない限り、プラットフォームで可能な限り高性能なデータ構造を取得するために Clojure を離れる必要がないということです。

deftype と defrecord

deftypedefrecord は、指定されたフィールドのセットと、オプションで 1 つ以上のプロトコルやインターフェースのメソッドを持つ名前付きクラスのコンパイル済みバイトコードを動的に生成します。それらは動的でインタラクティブな開発に適しており、AOT コンパイルする必要はなく、単一のセッション中に再評価できます。それらは、名前付きフィールドを持つデータ構造を生成するという点で defstruct と似ていますが、defstruct とは次の点で異なります。

  • 指定された名前に対応するフィールドを持つ一意のクラスを生成します。

  • 結果のクラスは、メタデータでの構造体の型のエンコードの規則とは異なり、適切な型を持ちます。

  • 名前付きクラスを生成するため、アクセス可能なコンストラクターがあります。

  • フィールドには型ヒントを設定でき、プリミティブにすることができます。

    • 現在、非プリミティブ型の型ヒントは、フィールド型またはコンストラクター引数を制約するためには使用されませんが、クラスメソッドでの使用を最適化するために使用されることに注意してください。

    • フィールド型とコンストラクター引数を制約することが計画されています。

  • deftype/defrecord は、1 つ以上のプロトコルやインターフェースを実装できます。

  • 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 定義で指定されていないキーを assoc できます)

    • など

  • 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 の両方が必要なのか?

ほとんどの OO プログラムのクラスは、実装/プログラミングドメインの成果物であるクラス(例:String やコレクションクラス、または Clojure の参照型)と、アプリケーションドメイン情報を表すクラス(例:Employee、PurchaseOrder など)の 2 つの明確なカテゴリに分類されます。アプリケーションドメイン情報にクラスを使用することの不幸な特徴は、情報がクラス固有のマイクロ言語の背後に隠されることです。たとえば、一見無害な employee.getName() でさえ、データへのカスタムインターフェースです。このようなクラスに情報を入れることは、すべての本が異なる言語で書かれているようなものです。もはや情報処理に対する一般的なアプローチを取ることはできません。これは、不必要な具体性の爆発と、再利用の不足につながります。

これが、Clojure が常にそのような情報をマップに入れることを推奨してきた理由であり、そのアドバイスはデータ型でも変わりません。defrecord を使用すると、汎用的に操作可能な情報に加えて、型駆動のポリモーフィズムとフィールドの構造的効率という追加のメリットが得られます。OTOH、ベクターのようなコレクションを定義するデータ型がマップのデフォルト実装を持つことは意味がないため、deftype はこのようなプログラミング構成を定義するのに適しています。

全体として、レコードはすべての情報伝達目的において structmap よりも優れており、そのような structmap を defrecord に移動する必要があります。structmap をプログラミング構成に使用しようとしているコードは多くはないと思われますが、もしそうであれば、deftype がはるかに適していることがわかるでしょう。

AOT コンパイルされた deftype/defrecord は、制限が禁止されていない場合、gen-class のユースケースの一部に適している場合があります。そのような場合、gen-class よりもパフォーマンスが向上します。

データ型とプロトコルは意見に基づいている

データ型とプロトコルは、ホスト構成要素との明確に定義された関係を持ち、Clojure の機能を Java プログラムに公開するのに最適な方法ですが、それらは主に相互運用構成要素ではありません。つまり、ホストのすべての OO メカニズムを完全に模倣したり、適応したりする努力はしていません。特に、次の意見を反映しています。

  • 具体的な派生は悪い

    • データ型は具象クラスから派生させることはできず、インターフェースからのみ派生させることができます。

  • 常にプロトコルまたはインターフェースをプログラミングする必要がある

    • データ型は、プロトコルまたはインターフェースにないメソッドを公開できません。

  • 不変性はデフォルトであるべきである

    • そして、レコードの唯一のオプションです。

  • 情報のカプセル化は愚かである

    • フィールドはパブリックであり、依存関係を回避するためにプロトコル/インターフェースを使用します。

  • ポリモーフィズムを継承に結び付けることは悪い

    • プロトコルはそれを解放します。

データ型とプロトコルを使用すると、Java 消費者に提供するクリーンでインターフェースベースの API を持つことになります。クリーンなインターフェースベースの Java API を扱う場合、データ型とプロトコルを使用して相互運用および拡張できます。 '悪い' Java API を使用している場合は、gen-class を使用する必要があります。この方法でのみ、Clojure プログラムの設計と実装に使用するプログラミング構成要素が、OO の偶発的な複雑さから解放されます。

reify

deftypedefrecord が名前付きの型を定義するのに対し、reify は匿名型を定義すると同時に、その型のインスタンスを作成します。これは、1つ以上のプロトコルやインターフェースの使い捨ての実装が必要で、ローカルコンテキストを利用したい場合に役立ちます。この点で、そのユースケースは proxy や Java の匿名内部クラスに似ています。

reify のメソッド本体はレキシカルクロージャであり、周囲のローカルスコープを参照できます。reifyproxy と以下の点で異なります。

  • プロトコルまたはインターフェースのみがサポートされており、具体的なスーパークラスはサポートされていません。

  • メソッド本体は、外部の関数ではなく、結果として得られるクラスの真のメソッドです。

  • インスタンスに対するメソッドの呼び出しは、マップのルックアップを使用せず、直接行われます。

  • メソッドマップでのメソッドの動的なスワップはサポートされていません。

結果として、構築と呼び出しの両方において proxy よりも優れたパフォーマンスが得られます。制約が過度に厳しい場合を除き、すべてのケースで reifyproxy よりも推奨されます。

Java アノテーションのサポート

deftypedefrecorddefinterface で作成された型は、Java 相互運用用の Java アノテーションを含むクラスを出力できます。アノテーションは、以下のようにメタデータとして記述されます。

  • 型名 (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)))