反パターン
というか、そんなことはどうでも良くて、
色々中途になっている話題もどうでも良くて(良くないか)、
デザインパターンである。
デザインパターンは私の高校生時代の心の拠り所で、
盲目的に信用していたものなのであるが、
関数プログラマに転身した(…あるいは、したい)現在となっては
それもどうかと思うものである。
プログラミングを行っていると、以前と同じようなことを繰り返しているなぁ、
と気づくときがあります。経験が増すにつれ、そのような「パターン」が自分の
心の中に数多く蓄積されます。そして、その「パターン」を次の開発に当てはめ
ていくことができるようになります。
(〜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]
これで十分だろう。