up: 📁Clojure Expression Problem

Clojure プロトコルとは

Clojure プロトコルとは, Javaインタフェースの代替のようなもの.

抽象データ型に対する📝Expression Problemへの一つのアプローチであり, よくprotocolの定義として登場する第一引数のthisとは操作対象データを示す.

別のアプローチとして📝Clojure マルチメソッドがある.

プロトコルのメリット

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

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

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

プロトコルの定義(defprotocol)

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

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

ref. defprotocol - clojure.core | ClojureDocs

Type/Recordとの連携(defrecord/extend-type)

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


defrecordからRecordを生成するときに, extend-typeを定義したファイルを事前にrequireしないと, 動的にプロトコルが追加でされてない状態生成されてしまうので, No implementation Errorになるので注意!

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

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

(fly (Bird. ))

しかし, defrecordで定義した場合はドット表記が必要.

✅Clojure Protocol Topics

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

💡defprotocolでオプション変数は定義できないが多変数は可能

defprotocolでは未サポート. できない, そして将来もサポートすることはない, あきらめろ(by 👨Stuart Sierra).


なお, 多重定義は可能.

;;  https://stackoverflow.com/questions/10645155/when-implementing-a-clojure-protocol-can-an-overloaded-method-call-its-overload
 
(defprotocol ClientProtocol
  (create-edge
    [this outV label inV]
    [this outV label inV data]))

definterface

defprotocolと似た関数でdefinterfaceがある. Java Interfaceを生成するマクロ.

https://clojuredocs.org/clojure.core/definterface

両者の違いは, 宣言時にthisと書いたり, 関数呼び出しでドット表記をつかうかのような違い.

definterface+(potemkin)

namespaceのみで利用するようなprivateのprotocolの場合, defprotocolよりもdefinterfaceのほうがメモリ効率がいい.

Clojureのdefintercaceはdefprotocolとちょっとシンタックスが違うのでdefinterface+を作ったらしい.

実際の仕様例(potemkinではないが作者が同じ人).

https://github.com/clj-commons/manifold/blob/master/src/manifold/bus.clj

Clojure Protocol Insignts

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

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

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

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

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

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

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-type

正しい利用方法なのかわからないけどメモ.

一つのRecordにいろいろとプロトコルを実装していくとファイルが大きくなりすぎて見通しが悪くなる. こういうときにextend-typeで別のファイルに実装を書いていくことはメンテナンスの観点でよい. 場合によってはnamespace単位で機能をコメントアウトできる.

あるnamespaceのRecordを別のnamespaceで参照するにはimportで読み込む必要があることに注意.

(ns Foo
  (:import (hogehoge.fugafuga Bar)))

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の分岐にそもそも手を加えたくないというところからインタフェースが検討された. さらにチーム開発で型をいじりたくないというところからプロトコルになった.

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

💡 一つのRecord固有の操作は関数/複数のRecordに共通の操作はprotocol

最近なるほどと思ったこと. たとえば hogehoge.libというnamespaceに defprotocolと関数両方書いておく.

(ns hogehoge.lib)
 
(defn who-are-you? [this]
  (:name this))
 
(defprotocol Region
  (where-are-you-from? [this]))

Recordを以下のように定義.

(defrecord Hoge [name place]
 
  Region
  (where-are-you-from? [this]
    (:place this)))

そうすると仮にこのHogeというデータを外部から操作しようとしたときに, libをrequireしてあげるともはやその実装がlibのnamespaceにあろうがdefrecordにあろうがあまり気にせずに, データに対する操作という形で呼び出せる.

(require '[hogehoge.lib :as lib])
(def hoge (Hoge. "aaa" "bbb"))
 
(lib/who-are-you? hoge)
(lib/where-are-you-from? hoge)

Clojureの世界にはクラスはいらない. Mapとそれに対する操作があればいいという世界感がより直感的に記述出来ている気がした.

なんかJavaのinterfaceの延長で考えても, protocolは複数recordに共通の場合のみ必要で単一recordには不要という説明も, あまり利用シーンのイメージがわかなかった. こういうことか. これがprotocolなのか…

💡protocolの第一引数thisとは対象の抽象データの操作を指す

考察続き.

データに対する操作というClojureの世界観をより忠実にするならば, 第一引数にMapを受け取りその操作をして返すという点で, たんなる関数によく thisが渡ってくるのにも納得.

defprotocolのthisってなんだよと思っていた. C言語のスタティックおじさんだったのでCの知識で考えるとthisとは構造体のポインタみたいなもんなんだ. そしてdefrecord誕生以前はまさにdefstractという構造体を提供する機能があったのだった. (ref. 💡defstructは古い(deplicated))

そうすると別にprotocolに限らずMapのデータを操作する関数の第一引数はthisでもいいんじゃね?と思った. なんかまとまった情報はまとめて渡しておいたほうが拡張性がある.

構造体とはバラバラな変数をまとめ上げるものであり, 一般的な用語では抽象データ型(ADT)である.

プロトコルの名前

IHogehogeだったり, Hogeableみたな名前をみく見る .

💡util関数がRecordにあるとClojureらしい(SimpleでElegant)ならばプロトコル実装

namespaceよりRecordフィールドの関数値のほうがシンプルでClojureらしいという観点

プロトコルとは関数の集合. そしてクラスとは, 値の集合と関数値の集合をひとつの連想配列にくっつけた集合. この観点から, プロトコルを単なるデータ構造と捉えるとすっきりする.

しかし, プロトコルの活用を🔖Expression Problemで語り始めると, 考慮することが増えて難しくなる. もうすこし簡単に考えたい. ポリモーフィズムの文脈が話をややこしくする.

本来, namespaceに関数をまとめておけばプロトコルは不要なんだ. しかし, Simple made Easyの観点から, 値とその操作関数はひとつにまとまっていたほうがシンプルであり, それはnamespaceよりもRecordに付随するフィールドとしての関数値のほうがよりシンプルなんだ.

言い換えれば, lazily conputed field(遅延計算)という観点.

Recordは分配束縛できるがプロトコルはできない

計算された値にアクセスする方法としてプロトコルをつかおうとするときに分配束縛がつかえないのが面倒であり, ここを考慮するとgetterのみを考えるようなフィールドは事前に計算した値をRecordフィールドとして事前に保持しておくほうがいいかもしれない.

やっかいなのは, primitiveなfieldとcomputed fieldが混ざっているときに部分的に分配束縛をつかうところ. シンプルではない.

References

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