動機

Clojureは抽象に基づいて記述されている。シーケンス、コレクション、コーラビリティなどの抽象が存在する。加えてClojureはこれらの抽象の複数の実装を提供している。これらの抽象はホストのインターフェース、実装はホストのクラスで定義されている。これは言語自身のブートストラップには十分だったものの、Clojureが同様の抽象化と低レベルの実装の機構を欠く原因になっていた。 プロトコルデータ型 という機能は、強力で柔軟な抽象化とデータ構造定義のメカニズムをホストプラットフォームの機構に対して妥協することなく提供する。

基本

データ型機能 - deftypedefrecordreifyは抽象の実装を定義するメカニズムを提供し、reifyについてはそれらの実装のインスタンスを提供する。抽象自体は プロトコルもしくはインターフェースによって定義される。データ型はホストの型(deftypeとdefrecordについては名前付き、reifyの場合は匿名)と何らかの構造(deftypeとdefrecordについては明示的なフィールド、reifyの場合は暗黙的なクロージャー)および任意の型の抽象メソッドの実装を提供する。これらは比較的綺麗な方法でホストの最もパフォーマンスの良いプリミティブへのアクセスとポリモーフィズム機構をサポートする。これらはただホストを括弧の中に記述するメカニズムではなく、ホストの機能の制限されたサブセットのみをサポートしており、多くの場合ホスト自身よりも動的である点に注意する必要がある。意図としては、ホストとの連携のために制限されたスコープの外に出る必要がない限り、プラットフォーム上で最もパフォーマンスの良いデータ構造を手にするためにClojureを離れる必要はない。

deftypeとdefrecord

deftypedefrecordは、与えられたフィールドと任意のプロトコル/インターフェースのメソッドを持つ、名前付きのクラスのコンパイル済みバイトコードを動的に生成する。これらは動的でインタラクティブな開発に向いており、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

    • メタデータのサポート

    • associativeのサポートト

    • フィールドのキーワードアクセサ

    • 拡張可能なフィールド(defrecordの定義にないフィールドのキーをassocすることができる)

    • など

  • deftypeはミュータブルなフィールドをサポートするが、defrecordはサポートしない

  • defrecordは加えて初期値のマップを受け取る#my.record{:a 1, :b 2}のリーダーフォームをサポートする:

    • defrecord名は完全修飾されている必要がある

    • マップ内の要素は評価されない

    • 既存のdefrecordフィールドはキーに対応する値をとる

    • リテラルマップにキーに対応する値のないdefrecordフィールドはnilに初期化される

    • 追加のキーに対する値は許容されており、defrecordに追加される

    • Clojureのバージョン1.3以降でのみ利用可能

  • defrecord Barが定義された場合、受け取ったマップの値でインスタンスの初期化を行う map->Bar が定義される(バージョン1.3以降)

なぜdeftypeとdefrecordの両方があるのか?

結論としては、ほとんどのOOプログラムは明確に2つに分類される:実装/プログラミングのドメインのもの(例えばStringやコレクションのクラス、もしくはClojureの参照型)とアプリケーションドメインの情報を表現するクラス(例えば従業員、購入注文など)だ。アプリケーションドメインの情報にクラスが使用されることは常に不幸な特徴であった。クラス特有のマイクロ言語の背後に情報が隠されてしてしまうことにつながるからだ。例えば一見無害なemployee.getName()もデータに対する特別なインターフェースだ。そのようなクラスに情報を入れることは、全ての本が異なる言語で書かれていたら問題になるのと同じように問題だ。情報を処理する際に一般的なアプローチを取ることができなくなり、無用な特殊性の爆発を招き、再利用を不可能にする。

Clojureが常に情報をマップに入れるように奨励してきたのはこれが理由で、データ型についてもこの奨励は変わらない。 defrecordを使用することによって、一般的に操作可能な情報に加えて、型駆動のポリモーフィズムというさらなる恩恵やフィールド構造による効率性も得ることができる。その一方でベクターのようなコレクションを定義するデータ型にマップのデフォルト実装を持たせる意味はなく、deftypeはそのようなプログラミングのための構造を定義することに向いている。

全体として、レコードは情報を持つあらゆる目的でstructmapよりも優れており、そのようなstructmapはdefrecordに移行するべきだ。プログラミングのための構造にstructmapを使用する可能性は低いだろうが、そのような場合にはdeftypeがはるかに向いている。

deftype/defrecordの事前コンパイルは制約が障害にならないいくつかの gen-class のユースケースに適していると思われる。そのような場合はgen-classよりもパフォーマンスが良くなる。

データ型とプロトコルには強い主張がある

datatypeとprotocolはホストの言語構造と明瞭な関係を持っており、Clojureの機能をJavaのプログラムに提供する良い手段ではあるものの、ホストとの連携のための言語構造ではない。つまりは、ホストの全てのOOのメカニズムを完全に真似したり適合させたりするための努力は一切していない。具体的には以下の意見を反映している:

  • 具象的な派生は悪手だ

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

  • 常にプロトコルもしくはインターフェースに対してプログラムするべきだ

    • データ型はそのプロトコルもしくはインターフェースで定義されていないメソッドを公開することはできない

  • イミュータブルがデフォルトであるべきだ

    • レコードについては唯一の選択肢となっている

  • 情報のカプセル化は愚かだ

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

  • ポリモーフィズムと継承を結びつけることは悪手だ

    • プロトコルを利用することでこれを避けることができる

データ型とプロトコルを利用すると、Javaの利用者にインターフェースに基づく綺麗なAPIを提供することができる。インターフェースに基づく綺麗なJava APIを利用している場合はデータ型とプロトコルを利用することで連携することも、拡張を行うことも可能になる。「悪い」Java APIがある場合は、gen-classを利用する必要がある。これがClojureプログラムを設計し、実装する際にOOがもたらす偶発的な複雑性を避ける唯一の方法だ。

reify

deftypeとdefrecordは名前付きの型を定義するが、 reify は匿名の型を定義しその型のインスタンスを作成する。ユースケースは、一度限りの1つ以上のインターフェース/プロトコルの実装でローカルコンテキストを利用したい場合だ。この観点からはproxyやJavaの匿名内部クラスのユースケースに似ている。

reifyのメソッドボディはレキシカルクロージャーで、周囲のローカルスコープを参照することができる。 reify は以下の点で proxy と異なる:

  • プロトコルもしくはインターフェースのみがサポートされており、具象スーパークラスはサポートされない。

  • メソッドのボディは外部の関数ではなく、結果として得られるクラスの本物のメソッドである。

  • インスタンスに対するメソッド呼び出しはマップのルックアップを行うことなく直接実行される。

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

この結果得られるのは、構築と呼び出しどちらにおいてもproxyより優れたパフォーマンスだ。 reify はその制約が障害にならない限り、あらゆるケースにおいてproxyよりも推奨される。

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

deftype、defrecord、およびdefinterfaceで作成された型は、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)))