Arowのこと

Haskellを勉強し始めて、モナドのことを知ってちょっとしたころに名前だけ見かけて、実際どんなものなのかを知りたいと思っていたのがArrowです。こちらhttp://www.haskell.org/arrows/で見つけられるような説明を読んだりしたのですが、もう、半年ぐらいちょっと読んでは挫折の繰り返しで、何を言ってるかわかるようになるまでずいぶんとかかりました...かといって、そんなに高度なことを言ってるわけではなくて、単に僕が時間がかかったというだけなんでしょうけど…

まだ、わからないことも色々あるのですが、とりあえず、ここまでの時点でわかったこと、書いておきたいと思います。

Arrowの導入の元になった論文の"Generalizing Monads to Arrows"によると、Arrowの導入のきっかけは構文解析器(パーサ)をデザインするにあったって、最初はモナドとしてデザインされていたのですが、メモリ消費特性の問題があってモナドクラスにはないクラスメソッドを導入する必要が出てきて、そこからArrowというクラスが出てきたとい言うことのようです。ただ、その前に圏論ではarrow(日本語で言うところの「射」というやつなんだと思うのですが...)というコンセプトはあったようで、それがソースになっているようでもあります。

論文のほうは、モナドでパーサを作ってみて実際どんな問題があって、だからArrowを導入しようみたいな話が出てくるのですが、それはとりあえず後回しにして、まずArrowのクラス定義を見てみます。

class Arrow a where
arr :: (b -> c) -> a b c
pure :: (b -> c) -> a b c
(>>>) :: a b c -> a c d -> a b d
first :: a b c -> a (b, d) (c, d)
second :: a b c -> a (d, b) (d, c)
(***) :: a b c -> a b' c' -> a (b, b') (c, c')
(&&&) :: a b c -> a b c' -> a b (c, c')

と、なにやら色々とクラスメソッドがあるのですが、必要最低限はarr(もしくはpure)と>>>、firstの3つだけで、後はそれらを使ってデフォルトのインプリメンテーションが提供されています。

はじめてみたときは、なんだかタプルが出てきたり、コンビネーターメソッドの名前が記号ばっかりで何がしたいのかぜんぜんわからなかったりでずいぶん混乱しました。なんですが、僕なりの理解はというと、まず、arrowとは何かを規定しているのは:

arr :: (Arrow a)=> (b -> c) -> a b c

で、つまり,Arrowはf:: b->cであるところの関数fにかぶせるカバーのようなものであるということです。

一体モナドと何が違うのでしょう?モナドのクラス定義をまたここに引っ張ってきましょう:

class Monad m where
(>>=) :: forall a b . m a -> (a -> m b) -> m b
return :: a -> m a

モナドのクラス定義で「モナドとは何か」を規定しているのは

return :: a -> m a

なんだろうと思います。それで何を言ってるかというと、つまり、モナドというのは中になんか入ってるということです。それは裏を返せば、中から何か取り出せるということで、その取り出すという行為が「アクション」であり、「計算」であり、モナド型のコンテナ性を規定しているようにも見えてくるわけです。コンピューターにとってはコンテナから何かを取り出すにも確かにコードを走らせなくてはいけないですから、立派に計算なわけですよね…

で、Arrowと何が違うのよといわれれば、モナドは中身を取り出すのにそのモナドそのものだけですむわけですが、Arrowは

arr :: (b -> c) -> a b c

なので、a b c のcが欲しかったら、bを突っ込んでやらなくてはいけない訳です。

だから、モナドは「計算」をパッケージしているのに対して、Arrowは関数をパッケージしているともいえそうですね…

じゃあ、関数をパッケージすると一体どう便利なのという話になるわけですが、そこで意味を持ってくるのがArrowのコンビネータメソッド:

(>>>) :: (Arrow a) => a b c -> a c d -> a b d

何だと思うんです。これはつまり、Arrowを使って何ができるかを宣言しているわけで、それぞれのArrowの値をそれが内包している関数としてみると、

a b c -> a c d -> a b d
= (b -> c) -> (c -> d) -> (b -> d)
= (b -> c) -> (c -> d) -> b -> d

ということで、何だ(.):: (b->c) -> (a -> b) -> a -> cにそっくりじゃんという話になってきます。実際Arrowのインスタンスの中には全ての関数(->)が含まれています。やってみると:

( (+1) >>> (*2) ) 4 
=> 10
( (*2).(+1) ) 4 = 4 
=>10

となります...そう思ってみると、関数をArrowという皮にくるむことで、イメージとしてはさまざまなArrowをプチプチとつなげて複雑なものが作り上げられるという感じのようです。

対比して、Monadで何ができるかを規定しているのが、

(>>=):: (Monad m) => m a -> (a -> mb) -> mb

なのだと思うわけです。これはつまり、モナドのつなぎ方を規定しているわけで、第一パラメタで示されるm aが内包している「計算」は第2パラメタである(a -> m b)が実行される前に実行されますといっていると解釈できますよね?つまり、Monadをつなぐと、Monadの実行は逐次行われるといっているわけですが、これに対してArrowの>>>は、もっとArrow同士のつなぎ方の記述だけに限定しているように見えます。
さらに、モナドのつなぎ方である>>=はつなぎ方を規定していると同時に実行もされるのに対して、>>>はつなげるだけで、実行のほうはされないですね...そうゆう意味で、あまり良いたとえではない気もしますが、(.)と($)の違いの様なものがありますね…

(+1).(+2) 1=> (\a -> 1 + (2 + a)) 1
(+1) $ (+2) 1 = (1 + (2 + 1))

それで、どこが便利なのという話なんですが、実は、冒頭で取り上げた論文でも説明されているのですが、arrと >>>だけではArrowはあんまり便利じゃないというのが実情のようです。(まぁ、(.)関数程度には便利なわけですが...)まぁ、関数をブロックのようにつなぎ合わせて色々やりたいと思ったら、>>>でできる直列つなぎだけじゃなくて、並列とか、分岐とか、ループとかできたほうが便利なのは確かですよね?

と、今回はここまでにして、続きはまた次回。

ではでは。