ノンフィクション「ビンゴゲーム」

近所(というか、家の真ん前)の夏祭りでビンゴゲームをした。
最初は7個中4個も当たってなかなかの滑り出しだったのだが、
その後がとんでもなく不調で、20個中3個ぐらいしか当たらなかった。
そうこうしているうちにどんどん当選者が出てみるみる
景品の等級が下がっていく。これはまずい。
そのとき、景品が残り7個というアナウンスが。
次の番号が告げられ続々と現れる当選者。
「・・・残り一つです」助かった。が、どう考えても次で最後だ。
リーチは2本。確率は20分の1といったところだろうか。
絶対に。絶対に勝たねば・・・!
そして最後の数がコールされる。
………
………
………
外れた。
見事に外れてしまった。
ショックに打ちひしがれていると、
近くで「当たった」という声がした。
そばに居た母がビンゴになっている。
どうやら4重リーチぐらいになっていたようである。
行ってきな、ということなのでありがたくチャンスを賜り、
かくして私は最終決戦の場に赴くこととなった。


当選者は私を含め7人。子供3人、おばさん3人。そして私。
この中で景品をもらえるのはただ一人、厳しい戦いである。
そして始まるじゃんけん勝負。
「さ〜いしょはグー、じゃ〜んけん」
チョキ!
勝った。
基本的に人間は前回よりも強い手を出したがるようである。
チョキを出しておけば負けることは無いと思ったが、やはりそうであった。
ここで子供3人とおばさん一人が脱落。
やはりお子様の心理は単純であった。
二回戦。
「じゃ〜んけん」
グー!
勝った。おばさん一人が脱落。
さすがにもう一人は出す手を変えてきた。
なかなかの強敵である。心してかからねばならない。
そして決勝戦。ここまで来たからには絶対に景品を持ち帰らねばなるまい!!
「じゃーんけん」
チョキ!
「あーいこで」
グー!
「あーいこで」
パー!
「あーいこで」
パー!


そして運命の一瞬。
「あーいこで」
チョキ!
…負けた。負けてしまった。
どうしてあそこでチョキを出してしまったのか。
悔やんでも悔やみきれない。
プレッシャーに負けてしまったとしか言いようがない。
そうなのだ。私はプレッシャーにめっぽう弱い。
パーが二回連続した時点で私の負けは決まっていたのかもしれない。


…かくして私のビンゴゲームは終わった。
私に勝った人が「8等賞」の景品をもらうのを横目に、
後でその中身が「石鹸何個か」だったことを知った。

反パターン

というか、そんなことはどうでも良くて、
色々中途になっている話題もどうでも良くて(良くないか)、
デザインパターンである。
デザインパターンは私の高校生時代の心の拠り所で、
盲目的に信用していたものなのであるが、
関数プログラマに転身した(…あるいは、したい)現在となっては
それもどうかと思うものである。

プログラミングを行っていると、以前と同じようなことを繰り返しているなぁ、
と気づくときがあります。経験が増すにつれ、そのような「パターン」が自分の
心の中に数多く蓄積されます。そして、その「パターン」を次の開発に当てはめ
ていくことができるようになります。

(〜Java言語で学ぶ デザインパターン入門 結城浩著 はじめに、より)


このこと自体にはまったくもって文句を付けられない。
その通りであると思うのだが、あるとき読んだ文章の中に、
このようなものを「デザインパターン」として堆積させていくのは
言語の抽象能力が低い所為なのではないのか。より強力な言語を
用いることによりパターンはパターンとしてではなくて
まさに存在するライブラリへと実装することが出来るのだ。
…というようなものがあった(ことばは適当)。
この言葉は一見して非常に魅力的(?)であり、
現に私は大いに考えを狂わされたのであるが、
これはどこまでも抽象的であり、最近ではほんとうに
そうなのかなぁと思ったり思わなかったりである。


やや具体的な言い分としては、十分に抽象化能力を備えた言語では
(GoFの)デザインパターン

  • そもそも必要ない

あるいは

  • ライブラリにできる


さて、これを検証してみることにする。
同じような試みは他の所にもありそうであるが、
とりあえず自力で納得のいくまで具体的に考えたい。


○第一章 Iterator 1つ1つ数え上げる
先の結城浩さんの本を読みながら考えることにする。
非常に単純にはこれは、コレクションから要素を引っ張ってくる方法の
統一的なインターフェースということになる。
C++ vs Haskellという図式で頑張ってみる。


例題は、本棚。

// iterator()でイテレータを引っ張ってきて、それで要素にアクセス。
class Aggregate{
public:
  virtual Iterator iterator()=0;
};
class Iterator{
public:
  virtual bool hasNext()=0;
  virtual void *next()=0;
};

class Book{
public:
  string getName(){ ... }
};
class BookShelf{
public:
  Book *getBookAt(int index){ ... }
  void appendBook(Book *book){ ... }
  int getLength(){ ... }

  Iterator iterator(){ ... }
};
class BookShelfIterator{
public:
  bool hasNext(){ ... }
  void *next(){ ... }
};

ええとまぁ、実装は書いてないけど、

int main(){
  BookShelf *b=new BookShelf();
  b->appendBook(new Book("Around the World in 80 Days"));
  b->appendBook(new Book(...));
  ...
  Iterator it=b->iterator();
  while(it->hasNexr()){
    Book *b=(Book*)it->next();
    cout<getName()<

のように使えるとする。
これをナイーブにHaskellに写し取ると、

class Aggregate a where
  iterator :: Iterator i => a i b -> i b

class Iterator i where
  enumerate :: i b -> [b]

data Book = Book String
getName (Book s) = s

data BookShelf i b = BS [b] ([b] -> i b)
emptyShelf = BS [] BSI
getBookAt  (BS bs _) n = bs!!n
appendBook (BS bs c) b = BS (b:bs) c
getLength  (BS bs _)   = length bs

instance Aggregate BookShelf where
  iterator (BS bs c) = c $ reverse bs

data BookShelfIterator b = BSI [b]
instance Iterator BookShelfIterator where
  enumerate (BSI bs) = bs

printIterator :: Iterator i => (a -> String) -> i a -> IO ()
printIterator p a = mapM_ (putStrLn.p) $ enumerate a

main =
  let b0 = emptyShelf
      b1 = appendBook b0 (Book "Around the world in 80 Days")
      b2 = appendBook b1 (Book "Bible")
      b3 = appendBook b2 (Book "Cinderella")
      b4 = appendBook b3 (Book "Daddy-Long-Legs")
  in printIterator getName (iterator b4)

IteratorをhasNext()とかではなく
enumerateとかいうものに変わっている。
まぁ、でも些細な点である。めんどくさいので。
重要なのは上でprintIteratorのような関数が定義可能であることだろう。
Haskellではキャストが出来ないので同じように実装するために
いろいろなところで頑張っている。


しかしここで、わざわざenumerateを経由してリストを返しているのが
とても冗長に感じられる。

class Aggregate a where
  enumerate :: a b -> [b]

data Book = Book String
getName (Book s) = s

data BookShelf a = BS [a]
emptyShelf = BS []
getBookAt  (BS bs) n = bs!!n
appendBook (BS bs) b = BS (b:bs)
getLength  (BS bs)   = length bs

instance Aggregate BookShelf where
  enumerate (BS bs) = reverse bs

main = 
  let b0 = emptyShelf
      b1 = appendBook b0 (Book "Around the world in 80 Days")
      b2 = appendBook b1 (Book "Bible")
      b3 = appendBook b2 (Book "Cinderella")
      b4 = appendBook b3 (Book "Daddy-Long-Legs")
  in flip mapM_ (enumerate b4) $ \book -> do
    putStrLn $ getName book

これでも汎化能力は変わらないのではないのか?
ここで問題となるのはなぜもとのコードでは
AggregateとIteratorが分離されていたかである。
それはHaskellの一番目のコードのprintIteratorのような、
Iteratorを引数にとるようなコードを実際のコレクションから
独立にさせるためであろう。
これが、Haskellの2番目のコードならば

printIterator :: (a -> String) -> [a] -> IO ()
printIterator = ...

main =
  let b0 = ...
      ........
  in printIterator getName (enumerate b4)

このように変わっていてもなんら問題はない。
無論printIteratorはBookShelf関係のデータ型からは独立である。


いったいどこが違うのか?
実のところどこも違わない。
C++(あるいは原著でのJava)でもこのような方法を取ることは可能であろう。
その際、イテレータを受け取る関数は、イテレータではなく、
listのような型を受け取ることになる。
で、Aggregateインターフェースに対してenumerateをリストを返すような
関数として定義すればiteratorクラスはいらない。
しかし、やはりこれは問題である。
何が問題であるか?それはおおよそ次の二つだろう。

  • 速度的問題
  • list型の扱いに関する問題

速度的問題は言うまでもない。
C++は正格評価がなされるので望ましくないコストがかかる可能性がある。
さらに、C++はリストに対して特別な最適化が行われることは無いだろう。


二番目のは、そのような用途にC++のlistを用いるのはややオーバースペック
では無かろうか?あるいはC++ではとくにlistが使いやすいわけではない。
iterator役をさせるのはどうなのか、という感じである。


その点ではHaskellのリストはここでのiterator役をさせるのに過不足無い
力を持っている。さらにリストのために使える関数が豊富に用意されている。


というわけで、おおよその結論としてはHaskellのリストはiteratorの役目を
果たすことが出来る。ということだろうか。
長くかいたわりにはいたって普通の結論。
リストがそのような能力を持つことが出来るのは高階関数を扱う力であろうか。
だからまぁ、C++でもFC++を使ってなんだかんだやればiteratorをリストとして
自然に記述できるのであろうが、その辺は趣味の話だろう。


実際にHaskellのコレクションの要素アクセスの方法だが、
Data.Setとか、FiniteMapとかをみると
xxToListという関数がある。やはりリストを使え、ということのようである。
列挙する能力を持つクラスを定めたければ、

class Aggregate a where
  enumerate :: a b -> [b]

これで十分だろう。

追記:一つのAggregateに複数のIterator

AggregateとIteratorが分かれていることで複数のIteratorを作れるようであるが、
iterator()のインターフェースが決まっているので、
どっちをもらうか選択は出来ないだろう。
しかしまぁ、どっちがきても問題にならないようにIteratorのインターフェースを
決めているのであるが、
ConcreteAggregateの中の状態に応じてIteratorを切り替えたいとしても、
そのようなことをするのに複数用意する必要があるのかはやや疑問である。
IteratorとAggregateが分かれていることにより、さらにもう一つ、
ConcreateAggregateがConcreateIteratorの実装から独立するというのが
あるようであるが、もともと依存性の高いものなので
独立させる必要は無いような(…たぶん?)