ClojureのAPI設計について考える
2016-07-19 / [clojure]
解決してないけど詰まったところをメモ。
やってたこと
複雑にmapとlistがネストしたデータ構造があるとする。 これらはユーザからJSONとかYAMLとかの形式で入力される。 親-子-孫のようにデータが木構造で5階層くらいまでネストしているデータ。 中のデータ型は一様ではなくて、mapだったりvectorだったりする。
この中から時刻を表すフィールドを読み出す。 時刻のフィールドには2種類あって開始時刻と終了時刻がある。 イメージ的には開始と終了時刻をもったタスクの塊だと思えばいい。
タスクが持つ時間の幅を数直線上での線分だと見なして、 下記のような操作を行いたい。
- 時刻をまとめて過去や未来にシフトする
- 時刻のスケールを変えて全体のタスクの時間を縮小したり拡大したりする
これは一次元のベクトルみたいなものだ。 なので一種のベクトルの操作っぽいものだと見なして抽象的に計算モデル作ることができる。 と思っていた。
難しいところ
原則的に大きなデータ構造の時刻フィールドだけを書き換えることになる。
Clojureにはupdate-in
という関数があってmap型のデータ構造がネストしていても、
keywordを並べるだけで目的のノードまでアクセスできる。
が、ここにlistやvectorが混ざると難しくなる。
また、データ構造を走査しながら時刻を書き換えるのは、 データの構造と時刻操作の関数が同じスコープの中で渾然と記述されがちであり特殊化されすぎるきらいがある。 例えばひとつの関数の中で木構造の特定ノードへのアクセスと、 時刻操作の計算が同時に現れるのは二つの操作の密結合とみなされる。 これが部品化を阻害するため関数型プログラミングのメリットをまったく享受できていない。
よって時刻の性質を取り扱うドメインと、具体的に構造へとアクセスするドメインを分ける必要がある。 それぞれを部品化して適宜組み合わせるようにしたい。
これを実現するためにどう設計したらいいのかとても難しいと感じた。
ネタ出し
ある複雑なデータ構造から時刻の側面だけを抽出したような表現が必要。 しかしもとのデータ構造の情報を失ってはいけない。一度失うと復元が難しい。 だから元のデータ構造にあるviewを与えてそのviewに対する操作が、 内部的には構造の走査を伴うアクションへと翻訳されなくてはならない。
これはHaskellのモナドであれば割と上手く形にできるんだけど、 Clojureではどうやっていいのかさっぱり解らない。
おそらくいくつかのProtocolを作ってそれをViewにするのだと思われるが。
やるべきことは下記のような気がしている。
- 時刻操作のviewを定義する
- 複雑なデータ構造にviewの実装を与える
- view上で時刻計算を行う
慣れないプログラミング言語でのデザインの難しさを知った。
状態機械という考え方(追記)
複雑なデータ構造と呼んでいるものは、 ある状態機械における状態の一種だと考えることができる。
状態機械に命令の列を一式与えて命令を簡約することで、 新しい状態が生み出されるという計算モデルを考えることはできる。
reduce
を使えばその名の通り命令セットを表すリスト(列)に初期状態を与えて簡約する操作は表現できる。
transducerの考え方に近い。
このアプローチでは命令セットを組み上げることが重要となる。 命令セットとは複数の合成された関数だと考えればよい。
これはTaPLに書いてあるラムダ項の簡約のモデルに例えると、 項(term)が状態を表す構造にあたり、評価関数が命令セットにあたる。
とまあ抽象化しまくるととても一般的なアプローチへといくらでも翻訳できるのだけれど、 問題はそれを実装する手段がClojureでまだ見つかってないというところで。
使えそうなAPI
データ構造のトラバースはzipperが良さそう。 ただ、mapとvectorの混在型では一筋縄でいかないことが解っている。 mapやvectorの塊をネストしたseqへと変換して操作すれば、 標準APIに入っているzipperでも扱える。 しかし、データの構造を変えることになり、構造の情報が保存されない(ロストする)のでできれば避けたい。