Arrowの柔軟性

id:MaD:20070820で書いたmapAの問題点ですが、ArrowLoopはそれを解決してくれるものではありませんでした。IOモナドやMonadFixについて勉強不足なので、これらについてしっかりと理解出来てはいないです。
問題が起こるのは、IOモナドとかStateモナドとか副作用のあるモナドを使用する場合でリストモナドやMaybeモナドなどを使用する場合は大丈夫です。

ただ、Monadに比べるとインターフェースが無駄に柔軟なので、無意識的にモナド再帰されたコードが出来てしまいやすいので注意しないといけなさそうです。pure,arrを書く位置がちょっと変わっただけで遅延評価が行われない反応の悪いプログラムになってしまうなどが起こってしまいます。


Arrowがどれだけ柔軟かという例を少し書いてみます。
(Kleisliといちいち書くのが面倒なので自分はKleisli -> K, runKleisli -> runKと置き換えて書いています。Kleisli.hs)

まず適当に+ 1するArrowを用意します

plus1 :: (Arrow a, Num b) => a b b
plus1 = pure (+ 1)

これは普通の関数として使えます。また普通の関数もArrowです。

> plus1 1
2
> show.plus1 $ 3
"4"
> map plus1 [1, 2, 3, 4, 5]
[2,3,4,5,6]
> (plus1 >>> show) 3
"4"

各種Arrowに繋ぐと自動的に変換されます。

Kleisli IO型に繋ぐ

> runK (plus1 >>> K print) 1
2
> runK (arr read >>> plus1 >>> K print) "3"
4

Kleisli Maybe型に繋ぐ

> let test = [("one", 1), ("two", 2), ("three", 3)]
> runK (K (`lookup` test) >>> plus1) "two"
Just 3
> runK (K (`lookup` test) >>> plus1) "three"
Just 4
> runK (K (`lookup` test) >>> plus1) "four"
Nothing

lookupで検索した結果をちょっと整形したいとか言う場合に便利かもしれないです。

Kleisli []型に繋ぐ

> runK (K (return [1, 2, 3, 4, 5]) >>> plus1) ()
[2,3,4,5,6]
> runK (K (\x -> [x,-x]) >>> K (\x -> [x, x*2]) >>> plus1) 1
[2,3,0,-1]

リストモナドと同じ挙動をします。普通の関数をpureでKleisli []に変換すると見た目上はmap動作になります。

Monadのコードと比較してみる。

適当に選んだふつけるMonadを利用するコードと比較してみます。

countline.hs

IOモナド

main = do cs <- getContents
           print $ length $ lines cs

Kleisli IO版

main = runK (K (const getContents) >>> pure lines >>> pure length >>> K print) ()

これでもいいですが、
自分は次の書き方の方がいいんじゃないかと思います。

main = getContents >>= (lines >>> length >>> print)

IOモナドの記法をベースにして関数合成の部分だけArrowの記法を使っている書き方です。
理由は

  • runKとかKとかは見にくい。(runKleisliとKleisliならなおさら)
  • KでArrowにして、runKでモナドに戻すという無駄がない。
  • 通常の関数を無駄にモナド化するのはどうかと思う。
  • モナドが知らず知らずのうちに再帰される問題は減る。

もう一つこの書き方で書いてみます。

tail.hs
main = do cs <- getContents
           putStr $ lastNLines 10 cs
lastNLines n cs = unlines $ takeLast n $ lines cs
takeLast n ss = reverse $ taken n $ reverse ss
main = getContents >>= (lastNLines 10 >>> putStr)
lastNLines n = lines >>> takeLast n >>> unlines
takeLast n = reverse >>> take n >>> reverse

このくらいなら見やすいかもしれません。

Kleisliについては、本来Monadにも(>>=)や(>>)などによって記法を改善するという目的があるわけですから、わざわざArrow化するメリットはないんじゃないかという感想です。
mapAなどのArrow用の汎用アルゴリズムを使ったり、コマンドコンビネータなどの記法を使いたい場合には必要になってくると思います。