モナドを触ってみた

で、こんな風に考えることにしました。

モナドは箱

モナドは単に値をくるんでるだけであると。
a をモナド化すれば m a。
でもってこうするための関数が return。単に m を付けるだけ。

モナドから値を取り出す

なぜか知らないのですが、m a -> a なる関数は存在してないようです。
謎です。
ただ、do 記法というのを使えば取り出せるようです。

main = let ma = return "hoge" in
       do
       a <- ma
       print a

return "hoge" でモナド化された ma は、do 内のみにおいてだけ a <- ma とすることで不思議な力が働いて値が取り出せます。何でこんなややこしいことするのか分かりませんが、とにかくそういうことらしいです。

モナド悲しみの連鎖

m a -> a なる関数が無いということは、一度モナド化したら、それを戻すことはできないということです。モナド化された値は一生モナドに囲まれて暮らしていくしかありません。do 内だけはモナドの囲いが取れますが、やはり限定的です。
モナド化するというのはそういった業を背負うことになるわけですね。あまり使いたくないです。

do 処理代行関数

do 内では何でもできるのですが、毎回書くのは結構面倒なせいか、いくつかの形式的な処理は do 無しでもいけるようになってるようです。
例えば >>= という演算子

   >>= :: m a -> (a -> m b) -> m b
ma >>= f = do
           x <- ma
           f x

とかまあそんなイメージになるようです。 f は m b を返す関数じゃないといけないところに注意です。

main = readFile "hoge.txt" >>= print

readFile は a->m b、print も a->m b です。つまり a->m b な関数を連続して繋ぐときに使うみたいですね。
じゃあ a->b な関数とか使いたくなったらどうしようとか思うかも知れないですけど、これは return.f とかするだけでいいです。m 付けるのは簡単ですね。

main = readFile "hoge.txt" >>= return.length >>= print

あと逆向きのもあります。

main = print =<< return.length =<< readFile "hoge.txt"

何のために使うんだろうとか思ってましたけど今日使いました。なのでこの記事を書き始めました。

素数表示

IO で出力してくる素数を表示する、というのをいろんな方法でやってみましょう。
n が素数かどうかを、配列の n 番目を調べれば分かるような配列を返してくれる以下の関数があるとします。

prime :: Int -> IO (UArray Int Bool)

で、この関数の結果を使って [2,3,5,7,11,...] と出力してみます。

printPrimes n = do
                ps <- prime n
                print $ getPrimes ps
  where
    getPrimes :: (UArray Int Bool) -> [Int]
    getPrimes = map fst . filter snd . assocs
main = printPrimes 100

モナドは出来る限り使わない方がいいということで、Bool 配列を素数列に変換する getPrimies 関数は普通の関数として作ってみました。


これでまあ終わりと言えば終わりなのですが、ここから printPrimes を do を使わずいろんな方法で表示してみましょう。結構楽しめました。

>>=を使って表示
printPrimes n = prime n >>= return.getPrimes >>= print

return が出てきました。これは a->b を a->m b として使う方法でしたね。

liftMを使って表示
printPrimes n = (liftM getPrimes $ prime n) >>= print

liftM は a->b な関数を m a->m b な関数に変換してくれます。
>>= は一度モナドを剥がして a->m b を呼び出すというジグザグなイメージですが、liftM された関数はモナド上で一直線に進んでるイメージですね。

ポイントフリースタイルで表示

引数に n があるのが気に入らないですよねってことで除けてみました。

printPrimes = (print=<<) . liftM getPrimes . prime

ここでついに =<< が出てきました。これが無いと

printPrimes = (>>=print) . liftM getPrimes . prime

となって方向的に微妙コードになるので、こいつがあって助かりました。

Control.Arrow で表示
printPrimes = prime >>> liftM getPrimes >>> (>>=print)

>>> 演算子は (->) 世界では flip (.) らしいので逆にするだけです。
・・・Arrow 使ってるのに何で liftM とか出てくるんだって話ですねすみませんちゃんと書きます。

Control.Arrow で表示(2)
printPrimes = runKleisli (Kleisli prime >>> arr getPrimes >>> Kleisli print)

いっつびゅーてぃふる。

おまけ:実装

どうでもいい部分ですが、上記の素数関数の実装はこんな風になりました。

prime :: Int -> IO (UArray Int Bool)
prime max = do
    ps <- newArray (0,max) True :: IO (IOUArray Int Bool)
    writeArray ps 0 False
    writeArray ps 1 False
    mapM_ (sieve ps) [2..isqrt max]
    unsafeFreeze ps
  where
    isqrt :: Int -> Int
    isqrt = floor.sqrt.fromIntegral
    sieve :: (IOUArray Int Bool) -> Int -> IO ()
    sieve ps i = readArray ps i >>= sieve' ps i
      where
        sieve' ps i True  = mapM_ (flip (writeArray ps) False) [i*2,i*3..max]
        sieve' ps i False = return ()

flip してる部分が何かパッと見だと分かりにくいように感じますが、単に \n->writeArray ps n False をラムダ式無しで書いてるだけです。ラムダ式書いたら負けです。
あと引数もポイントフリーにしたいですがさすがに辛いのでやめました。

まとめ

最後の方はモナドとか関係無かった気もしますけど、まあモナドとかよく分からなくても使うだけならできそうな感じですね。