📝Clojure: プロトコル(Protocols)

📝Clojure: プロトコル(Protocols)

Clojureプロトコル. Javaインタフェースの代替.

up: 📁Clojure Expression Problem

プロトコルの定義 | defprotocol #

defprotocol でプロトコルとメソッドを定義する.

extend (またはそのマクロであるexpand-type/expand-protocol)で定義したプロトコルを型に適用する. またはdeftype/defrecordによって型を定義するときにはじめから組み込む. 前者は動的に型を拡張している.

ref. defprotocol - clojure.core | ClojureDocs

プロトコルのメリット #

組み合わせ可能な抽象化 (📚プログラミングClojure)

Javaのインタフェースでは新たな操作を型に追加するときそのデータ型の定義を変更する必要がある. プロトコルならば型の定義をいじることなく操作を追加することが可能.

また異なるプロトコルに同じ名前のメソッドが定義されているときも動的に組み込むため名前の競合によってエラーが発生しない.

Type/Recordとの連携 #

defprotocolで定義したプロトコルは Clojure Records に組み込むことができる.

(defprotocol Fly
  (fly [this] "Method to fly"))

(defrecord Bird [name species]
  Fly
  (fly [this] (str (:name this) " flies...")))

Recordには複数のProtocolを実装することが可能(cf. mix-in, 多重継承).


すでに定義したtype/recordにあとから protocolを追加するには extend-type をつかう.

(defprotocol Fly
  (fly [this] "Method to fly"))

(defrecord Bird [name species])

(extend-type Bird
  Fly
  (fly [this] (str (:name this) " flies...")))

extend-typeであとから拡張するのとdefrecordではじめから定義することはやりたいことは同じだがJava実装レベルではやっていることがちがう.

ref. clojure extend-type vs deftype and protocol implementation

定義したプロトコルの呼び出し #

定義したプロトコル実装を呼び出すにはJavaのオブジェクトメソッドのようなドット表記は不要.

(fly (Bird. ))

Record, つまりMapのkeyにbindされた無名関数をコールしているのでやっていることは(get (Bird. ) fly)に近い.

extendは既存の型の拡張であり新たな型の定義ではない #

extendは動的に既存の型を拡張するので、型の種類が増えるわけではない. 新たな型の定義はdefrecordで定義する.

Clojureでは親子関係の継承をサポートしない. たとえばOOPでいうことろの親に共通のデータや振る舞いを持ち子で一部を変更したい場合, 1つのプロトコルを定義してそれらを複数のレコードを定義する中に組み込む.

Clojureで階層関係を表現するシンタックスでderiveというものがあるがこれはtagを引数にとるものでRecordとは直接関係しない.

Howto #

Clojure Protocolsの定義と実装を異なる名前空間で分けるには?(GoF Bridge) #

ちょっとしたトリックが必要.

ProtocolとRecordの定義が異なる場合はメインの名前空間でrecordとprotocolの名前空間をrequireしてさらにdefrecordの定義をimportする.

ref. Clojure Protocol Namespaces — Matthew Boston

(ns company.core
  (:require [company.car]
            [company.drives :refer [drive]])
  (:import company.car.Car))

(defn -main [args]
  (println (drive (Car. 60.0) 0.4)))

問題はサブの名前空間で定義したプロトコル実装を外部公開したいとき, 設計上はメインの名前空間だけを外部に公開してサブは隠したい. これは Bridge Pattern にほかならない.

Protocol定義とメインの2つをrequireすればいいよと すべてメインに入れて移譲すればいいよという意見がstackoverflowにありこのへんは好みかも.

ref. clojure keeping protocol definition in a separate namespace from implementation - Stack Overflow

Insignts #

プロトコルの関数実装はメソッド #

📚プログラミングClojureでは定義したプロトコルの関数実装をあえて オブジェクト指向パラダイムのメソッドという表現をつかっていて, 関数型パラダイムの関数とわけている.

extend-x比較(extend vs extend-type vs extend-protocol) #

どれもJavaの継承の代替を実現するためにつかう.

そして source をみるとextendは関数であり, extend-type/extend-protocolはマクロなようだ. やっていることはextendの定義を書きやすくしているに過ぎない. clojuredocsに乗っている実例でマクロ展開前とあとを比較するとよい.


If you are supplying the definitions explicitly (i.e. not reusing exsting functions or mixin maps), you may find it more convenient to use the extend-type or extend-protocol macros.

つまりextendしたものを再利用しないならばmacroが便利ということ?


extend-type : 単一の型に対して複数のプロトコルの実装をかく.

Useful when you are supplying the definitions explicitly inline.

つまり1行程度の簡易メソッドをRecordに生やしたい場合とか.


extend-protocol : 複数の型に対する単一のプロトコルの定義をかく.

Useful when you want to provide several implementations of the same protocol all at once.

protocol自体が操作抽象であり操作のグルーピングを目的にしているのがextend-typeはデータを起点にまとめるか, extend-protocolはメソッドを起点にまとめるか.


ref. extend-type and extend-protocol: different syntax for multi-arity methods

extend-protocolとextend-typeもおなじことができる. 質問者はややこしいよといっている. まあどちらもextendから派生したmacroなのでいいのでは?

extend-protocol vs multimethod #

extend-protocolとdefmultiは同じようなことができる. ただしprotocolは関数のはじめの引数の型でしか処理を分岐できない. いっぽうmultimethodは引数だろうがそれを分解した内部データ鳴り計算結果なり… なんでもできる.

;; Multi
(defmulti foo class)

(defmethod foo java.lang.Double [x]
  "A double (via multimethod)")

(defmethod do-a-thing java.lang.Long [x]
  "A long (via multimethod)")


;; Protocol
(defprotocol Bar
  (bar [x] "..."))

(extend-protocol Bar
  java.lang.Double
    (bar [x] "A double (via protocol)")
  java.lang.Long
    (bar [x] "A long (via protocol)"))

黒魔術である動的Mixinとしてのextend-protocol #

extend-protocolはGeneralな概念を考えると 動的Mixinかもしれない. そして他の言語だとメタプログラミングによって実現するものを, Clojureだと普通にできるに過ぎないのかも.

動的に操作を組み込むことはチーム開発対策か #

ref. プロトコルのメリット

動的というのがポイントかも. まず前提としてifやswitchの分岐にそもそも手を加えたくないというところからインタフェースが検討された. さらにチーム開発で型をいじりたくないというところからプロトコルになった.

既存の型定義をいじらないでの追加だったり, 名前の競合を選択的に解決だったりは, これはおそらくチーム開発で別々のプログラマが開発しているときの課題解決を意識している気がする. 大規模に慣ればなるほど既存コードは弄りたくないし, 複数人で同じコードはいじりたくない.

References #

情報少ないなあ… 特に日本語. 書籍読むのがいい.


Tags