Clojure

プロトコル

動機

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

プロトコルにはいくつかの動機があります

  • インターフェースの代替として、高性能で動的なポリモーフィズム構造を提供する

  • インターフェースの優れた部分をサポートする

    • 仕様のみで、実装は含まない

    • 単一の型が複数のプロトコルを実装できる

  • いくつかの欠点を回避しながら

    • どのインターフェースが実装されるかは、型の作成者による設計時の選択であり、後で拡張することはできません(ただし、インターフェースインジェクションによって最終的にはこれが対処される可能性があります)

    • インターフェースを実装すると、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"))
  • 実装は提供されません

  • プロトコルと関数にドキュメントを指定できます

  • 上記は、一連のポリモーフィック関数とプロトコルオブジェクトを生成します

    • すべて定義を囲む名前空間によって名前空間修飾されます

  • 結果の関数は、最初の引数の型に基づいてディスパッチされるため、少なくとも1つの引数が必要です

  • defprotocolは動的であり、AOTコンパイルを必要としません

`defprotocol`は、プロトコルと同じ名前の対応するインターフェースを自動的に生成します。たとえば、プロトコルmy.ns/Protocolが指定されている場合、インターフェースmy.ns.Protocolが生成されます。インターフェースにはプロトコル関数に対応するメソッドがあり、プロトコルはインターフェースのインスタンスで自動的に機能します。

`deftype`、`defrecord`、または`reify`でこのインターフェースを使用する必要はありません。これらはプロトコルを直接サポートしているためです。

(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は、型/クラス(またはインターフェース、下記参照)、1つ以上のプロトコル+関数マップ(評価済み)のペアを取ります。

  • 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"