wcコマンドの実装を通してArrowの気持ちを推し量ってみた

"モナドの一般化"なんていう敷居がやたら高いふれこみのArrowをちょろっと調べてみた。
主に、ha-tanさんのサイト(http://d.hatena.ne.jp/ha-tan/20070810/1186744818)と、そこから参照されているページをひととおり読んだ。

モナドがわかる人には多分、

  • Monadは、値をからくり箱に入れたもの。からくり箱は基本的に(ヘンな)値。
  • Arrowは、関数をからくり箱に入れたもの。からくり箱は基本的に(ヘンな)関数。
3分で解るHaskellのArrowの基本メモ - よくわかりません

という説明でなんとなく通じるかなあ。

Monadが型クラスなようにArrowも型クラス。でもって、Arrowの場合はからくり箱の中身が関数なので、型情報によって処理をふりわけるっていう性質がモナドより強い(気がする)。ストリームファンクションはそのよい例で、関数の型を、Arrowのインスタンス型のどの型だと推論/定義するかで、関数自体の定義は全く変えずに関数の対象が値からリストにがらっと代わる*1
まだ試してないけど、Arrowとして定義されている関数には、Arrowのインスタンスの型を後付けしてやることによっていろんな処理ができるんじゃないかなあ。多分、関数の返り値を全部ログに取る、なんてのも書ける気がする。無理かな。書けなかったらごめんなさい。

で、http://d.hatena.ne.jp/ha-tan/20070814/1187046533を参考にwcコマンドの実装をしてみた。確かにタプルでパターンマッチさせるのはいまいちだな、と思ったのでさらに、http://d.hatena.ne.jp/takkan_m/20070814/1187102348を参考にパターンマッチをほんのりラップしつつ、自分なりにArrowの気持ちが一番わかりやすい形で組みたててみた。

import Control.Arrow

wc :: (Arrow a) => a String (Int,(Int,Int))
wc = count_lines &&& count_words &&& count_chars
    where
      count_words = arr (words >>> length)
      count_lines = arr (lines >>> length)
      count_chars = arr length

a3_show :: (Arrow a,Show b) => a (b,(b,b)) String
a3_show = arr (show *** show *** show) >>> join
    where
      join = second join_tab >>> join_tab

join_tab :: (Arrow a) => a (String,String) String
join_tab = arr (\(a,b) -> a ++ "\t" ++ b)

main = runKleisli (Kleisli (const getContents) >>> 
                   wc >>> a3_show >>>
                   Kleisli putStrLn) ()

"Kleisli"というのはモナドを内部に含むようなArrowのインスタンス型であり、かつ、"Kleisli m"型のデータ構築子でもある。"runKleisli"というのが、Kleisli型の値からモナドな関数をひきだす関数で、"Kleisli m a b -> a -> m b"という型を持っている。この場合は、"Kleisli putStrLn"等々からIOモナドを含むKeisli IO型の値を生成し、そこから"runKleisli"を使って"a -> IO ()"型の関数を引きだしているので、mainの型は"IO ()"となって、型の整合性はとれている。
見てのとおり、wcやa3_showにはモナドのことは一言も書いてないけど、">>>"を使ってArrowとして結合してあるので勝手にArrowのインスタンスである"KLeisli IO"型に推論されていて整合性がとれる、と。これは地味に嬉しい。

あと、wcとa3_showには、うざったい型宣言が書いてあるが、これは省略してもちゃんと動く。なんで書いてあるかというと、型宣言を省略すると、mainの部分にひっぱられてwcが"wc :: Kleisli IO String (Int, (Int, Int))"と推論されてしまうから。"Kleisli IO"はArrowのインスタンス型で、つまり、省略すると本来はArrowという型クラスでいいはずの型が具体的な型に推論されてしまう。

さっきも書いたけど、多分Arrowのうまみは(多分)型情報によって処理を切りかえるところなので、具体的な型になってしまうとそのうまみがなくなっちゃうと思う。例えば、上のように型宣言しておいたソースをGHCiで読みこむと、

Main> wc "one two\three"
(1,(3,12))

なんていうのも動作する。実は、普通の関数の型 "->"もArrowのインスタンスとして定義されているので、この場合はwcが普通の関数 "String -> (Int,(Int,Int))"と推論されて動作してるわけだ。wcの型が具体的な"Kleisli IO ..."と推論されてしまっているとこのコードは型エラーで動かない。

乱暴にまとめると、Arrowはモナドみたいにシーケンシャルな結合だけじゃなくて、分岐や(ここでは使ってないけど)繰り返しを表現する演算子を使って結合させることができて組み立ての自由度が高い。さらにArrowな演算子で実装した関数は(ちゃんとArrowな型宣言を書いておいてやると)いろんなArrowのインスタンス型と整合してくれる。上のコードの中ではwcを一行も書きかえることなく、IOモナドな関数および普通の関数として動作してくれる。Arrowをうまく使うと非常に再利用性が高い関数群が定義できるはず。

ということは、Arrowの使いこなす上で鍵になるのは、Arrowのインスタンス型をいかに上手く定義できるか、ということになるような気がするんだけど、他に典型的なArrowのインスタンスってどんなのがあるのかな。次はそのへんかなー。

*1:http://d.hatena.ne.jp/propella/20070807/p2にでてくるSFがその例