デザインパターン読書会 #1
@cesare さん主催のイベント。
- 作者: 結城浩
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2004/06/19
- メディア: 大型本
- 購入: 51人 クリック: 762回
- この商品を含むブログ (400件) を見る
趣旨は少なくともそうだったはず。
ごめんなさい、なんだかディープな雑談ばかりになってしまった。
もっとベーシックな方向で話をしたかったですね。
Iterator パターン
今回は第1章でIteratorパターン。
最初から、Iterator パターンは while ループが外に出たままで、抽象化が不完全、という話が出た。いわゆる、外部イテレータと内部イテレータの話ですね。
Smalltalk だったら例えば*1、
#(1 2 3) do: [:n | Transcript cr; show: n]
というふうに、do: に処理をブロックで渡して、Array 側で処理してもらえるわけで、こういうほうがいくね? っていう。
まあ、私も外部イテレータは気持ち悪いと思うんだ。けど、メソッドそれ自体を引数に渡すって手段がないJavaという言語なら、しょうがないかなあって感じる。でもなぁ、C++なら関数ポインタでイケる気がするんだけど。
ともかく、「データ構造が違っても同じように使える」は、十分嬉しい抽象化だと思うので、それはそれでいいと私は思う。
私としては一番のふむふむポイントは、AggregatorとIteratorを分離することで、Iteratorをnewするごとに「最初から値を取り出す」って処理をどんどこ行うことができるってことかなと。
あとスレッドセーフの話が出ましたね。イテレーションしてる間に外部からデータ書き換えられちゃったらどうするんだと。まあ、それについては応用編なので、結城先生の別の本を読みましょうってことで。
大事なのはデータ構造がどうなってても、hasNext と next という統一したインターフェースで処理が記述できるってことなんだよって話になって、配列とか線形リストとか木構造とか、あるいはファイルシステムだとかデータベースだとかそういうことをぜーんぶ無視して同じように扱えるってことだよねって話をした。
ここで「木構造でnextを実現するには継続が必要じゃない?」という発言が出て、えっ、って感じで、いやいやそんなことないでしょう、次を返すのに必要な情報さえあれば十分なんだから、継続とか持ち出さなくても大丈夫でしょう、という話をしたんだけど、これが後でモメるネタになるとは。
あとGoFだとfirst/get/nextという構造なのに、結城先生の本だとhasNext/nextという作りなのはなんでだろ、という話になったけど、これは多分、GoFだとC++だから
for (it.first() ; !(it.isDone()) ; it.next()) {
val = it.get()
}
と、for文が綺麗に書けるからってことかなぁって話になったような気がする。
じゃあなんでJavaではそうしなかったの? というと、while一個で簡潔に書きたかったからなのかなぁ。
例えば first() も next() も要素が返せなかったら null を返すようにして、
book = it.first() while (book != null) { System.out.println(book.getName()); book = it.next() }
みたいな書き方 (それどこの LotusScript? という声が聞こえた) もできるだろうけど、イディオムとして冗長だし、返却値で動き分けるのって綺麗じゃないよね、C じゃないんだから。だから、妥協点として正しい気がしてきた。
話戻って外部イテレータ対内部イテレータ
外部イテレータってイケてないよねなコト。
無論?私もそう思うんだけど、なんでそうなのかな、と帰る道々考えた結果、次のような屁理屈をあみ出した。
外部イテレータと内部イテレータの大きな違いは「制御構造を隠蔽しているか、外に出ているか」なんだけども、それによって生じる内部イテレータの大きな利点の一つは、制御構造を隠蔽することによって、データ構造に最適なイテレーション (例えば再帰とか) が容易に選択可能なことなんじゃないか、と。
でも本質論からいうと、それって言語の持つ再帰という機能によってスタックフレームに状態の保持を押し込めているだけで、状態を保持しなければイテレーションはできないんだから、それをどうやるかという手段の話でしかない、とも、私は思うのです。
いや、実装する人間としては、きれいに書けることは嬉しいが :)
継続についてあれこれ
そこでついったで一モメした*2 継続について。
えーと、ああいう文脈で「継続」といわれると、もちろん Scheme や Ruby や Smalltalk が言語機構として持っている継続が浮かびますね。
でも先程の再帰の話と同じく、言語として用意されているかどうかは本質じゃなくて、「継続」という概念ってのは何の抽象化なのか、ってのが大事だと思う。
イテレータの例でいうと、配列のイテレーションならindexを内部状態として保持しておけば、next() は作れる。簡単ですね。
でも木構造の場合、スタックに分岐点のノードオブジェクトの参照をプッシュしていって、葉までいったらそれを返すとともに、スタックから分岐点を取り出して次の枝に降りていく、ということをやらなきゃいけない。めんどくさいですね。めんどくさいけど、データ構造に見合った処理をしてるだけです。つまりぜんぜん抽象化していない。めちゃんこ具象です。
そして私は「継続」という「概念」を、上の二つの例を、その具体的な処理内容に依らず、途中で止めて、任意のタイミングで再実行できるしくみを用意することだと自分の中で定義しているわけです。これは、前記の言語が提供している機能がそうなので、言語設計者が抽象化したかった概念がそうだと解釈しています。
もちろん、それは言語にビルトインされていてもいいし、自前で頑張って書いてもいいけど、イテレータがそのイテレートする対象のデータ構造を隠蔽しているが如く、継続においては継続の対象となる処理を隠蔽していなければならない。これは必須だと考えています。
それを実現するためには、実行コンテクストを保持しておくって実装をエイヤコラと書かねばならん。とっても大変だと思うんですが。というか、普通の言語だとそこまで言語自身で制御できないのが多いんじゃないかな。
Z80で実装できるって話が出てたけど、むしろZ80ならできるでしょう。Cでもできるかもしれない……んーインラインアセンブラの力を借りないと難しいか? 普通の言語だと実行コンテキストなんてものは隠蔽されてしまっているから触れないから、非常に難しいと思うなぁ。
あ、Smalltalk ならコンパイルされたバイトコードを手で触れるので、こういう実装は得意……だそうです。受け売り ;-P
つことで、ぼくが「概念」として持っている「継続」は言語の上で実現するのは非常に難易度が高く感じるので、そこまで大げさな話じゃないでしょう? というのが、「木構造のイテレーションだって継続なんか要らない」という主張なんです。
もちろん、「止めてたものを再開できる仕組みなら全部継続」という立場もあるでしょう。でもそうしたら、配列のindexを内部状態として保持する、という実装を「継続」と呼ばない理由がない。「木構造には継続が必要」なんではなくて「イテレートにはすべて継続が必要」ということになります。当たり前すぎますね。
それが正しいのであれば……そんな当たり前なものに名前つけて「概念」だなんていうのは大仰というかもったいないなぁ、というのが私の率直な感想。
という話を140文字で表現しようとした私が悪かったです。猛省しております……。
おわりに
参加して議論に加わってくれた皆さんありがとうございます。
ぼくは Java はぜんぜんわかんないんで、色々勉強になりました。
ただ時間に余裕があるなーと思うと脱線してしまう傾向があるので、2章ぐらいずつやってもいいかなとぼくは思いました。
それとついったでギスギスした雰囲気にしちゃったことは本当に反省しています。
これに懲りず、次回も楽しく議論できればな(できれば脱線方向ではなく、基本に立ち返る方向で ^^;) と思います。