2013年9月28日土曜日

モナドの実際の使い方

私の感覚としては、モナドはかなりわかりづらい。モナドの定義とか計算法則はわりとわかる。以下サイトがおすすめ。

Haskell の State モナド (1) - 状態を模倣する | すぐに忘れる脳みそのためのメモ:
http://jutememo.blogspot.jp/2009/10/haskell-state-1.html

しかし実際にプログラムを書いてみると、使い方がわからない。特に IO モナドがわからない。実際にプログラムを書いて困った点、それらをどのように解決しているか? ということをまとめる。
今から振り返ると Real World Haskell の内容ちゃんと理解して読んでたら問題にすらならなかったんですけどね。

1. 副作用の分離?

モナドは副作用のある計算を分離するなど言われる。確かに多くのモナドでは純粋な関数からモナドを使用し、run することにより値を取り出すことができ、状態によって結果が変わるかもしれないコードを分離することができる。素晴しい。
ただし、最もよく使われるであろう IO モナドは値を取り出してモナドから純粋な関数に戻ることができない (unsafePerformIO は考えないことにする)。つまり、「IO モナドの関数は IO モナドの関数からしか使用できない」。
例えば以下のようなコード。
-- Test1.hs
import Data.Char

main = do
    let s = func0
    func1 s

func0 = "s"

func1 s = func11 $ s ++ s
func11 = func12 . map toUpper
func12 = putStrLn
func1 > func11 > func12 と続く連鎖のなかで、IO を使用している関数は func12 のみだが、その間で純粋な関数しか使用しない func1, func11 も連鎖して IO モナドの関数になってしまう。
これは非常に扱いづらいように思える。どんなプログラムでも、関数の連鎖の一番先に IO 使用する場合、そこに至る全ての関数は IO モナドにしなくてはいけない。これは 「IO モナドが伝染する」 と言われたりするよう。
上の例だと単純に書き方が下手なだけだけど、例えばネットワークプログラミングをするとしばしば利用するであろう inet_ntoa なんかは副作用なんてあるわけなさそうだが、C の関数を利用するからという理由で IO になっている。実際のプログラムを書くにあたってこの問題は避けて通れないと思う。

2. グローバル変数?

これも副作用を排すという Haskell の基本ルールだが、グローバル変数が使用できない。ただ、これも実際にプログラムを書くといくらでも使いたくなる。こういった性質の変数がないと、「状態」 を使用するプログラムが極端に書きづらくなる。
以下、ユーザから入力を受け取り、その状態に基づき出力結果を変えるプログラムのサンプル。
-- Test2.hs
import Data.Char

main = do
    s <- func0
    func1 s
    func2 s

func0 = do
    putStrLn "Enter some string:"
    getLine

func1 s = do
    putStrLn "Repeat plese:"
    s' <- getLine
    putStrLn $ if s == s' then "Correct"
                          else "Wrong"

func2 s = do
    putStrLn "In upper cases please:"
    s' <- getLine
    putStrLn $ if (map toUpper s) == s' then "Correct"
                                        else "Wrong"
当然だけれど最初に入力した文字列をその後の処理を行う関数に渡す必要がある。実際のプログラムではこの関数がさらに別の関数を呼び…と続き、引数の引き回し地獄となること必須。こんなんだったらグローバル変数使わせてくれ、と思ってしまう。

3. モナドの実際の使い方

では、これらの問題をどうやって解決するかということで、実際のプログラムを見てみる。私の Haskell プログラムのリファレンスはだいたい xmonad です。今回のコードは xmonad-0.11 より。
-- Xmonad/Main.hsc
xmonad :: (LayoutClass l Window, Read (l Window)) => XConfig l -> IO ()
xmonad initxmc = do
    -- snip --
    allocaXEvent $ \e ->
        runX cf st $ do
            -- snip --

            -- main loop, for all you HOF/recursion fans out there.
            forever $ prehandle =<< io (nextEvent dpy e >> getEvent e)

    -- snip --
省略した部分は基本的にメインの処理に至るまでの前準備で、ポイントはメインのループ処理。ここで、ユーザからの入力やアプリケーションの状態変化を受け取り、その状態に応じて処理を行う。この関数は "X" モナドになっており、それが runX によって走らされている。X モナドの定義は以下。
-- XMonad/Core.hs
-- | The X monad, 'ReaderT' and 'StateT' transformers over 'IO'
-- encapsulating the window manager configuration and state,
-- respectively.
--
-- Dynamic components may be retrieved with 'get', static components
-- with 'ask'. With newtype deriving we get readers and state monads
-- instantiated on 'XConf' and 'XState' automatically.
--
newtype X a = X (ReaderT XConf (StateT XState IO) a)
    deriving (Functor, Monad, MonadIO, MonadState XState, MonadReader XConf, Typeable)

-- | Run the 'X' monad, given a chunk of 'X' monad code, and an initial state
-- Return the result, and final state
runX :: XConf -> XState -> X a -> IO (a, XState)
runX c st (X a) = runStateT (runReaderT a c) st
コメントのとおりだが、IO, State, Reader モナドをトランスフォーマーによって組み合わせたものが X モナドである。X モナドの関数の中で get / set (State) すれば XState を操作、ask (Reader) すれば XConf を参照できる。また、X モナドは MonadIO のインスタンスになっている。このことにより、X モナドの関数中では liftIO を使用することにより IO の関数を呼び出すことができ、見かけ上 IO モナドの伝染を防ぐことができる (なお、xmonad では io = liftIO としている)。

上述の問題で悩んでいるとき、State モナドの存在は知っていたけど、メインの処理を全てモナドで包んでやるという発想が出てこなかった。これだと、グローバル変数使うのと何が違うのか? という疑問も出るが、少なくともその変数の所在が明らかになるという点でメリットはあるのではないかと思う。

参考

Real World Haskell Chapter 18. Monad transformers: http://book.realworldhaskell.org/read/monad-transformers.html
本物のプログラマはHaskellを使う - 第29回 グローバル変数の代わりに使えるReaderモナドとWriterモナド:ITpro: http://itpro.nikkeibp.co.jp/article/COLUMN/20090303/325807/

2 件のコメント:

  1. <を表示するための&が$になってます

    返信削除
    返信
    1. ご指摘ありがとうございます。修正しました。

      削除