Prev: Part III - イントロダクション TOC: 目次 Next: モナド変換子

モナドの合成は難しい


モナド変換子の使い方を詳細に調べる前に、モナド変換子を使わずにモナドを 合成する方法について見ておきましょう。モナドを合成する際の問題について 洞察を展開するのに、これは良い練習問題になります。そして、変換子を使う アプローチを計るベースラインを提供することになります。 example 18 (Continuation モナド) のコードを使って問題点を説明しますので、先へ行くまえに復習しましょう。

入れ子になったモナド

いくつかの計算は単純な構造なので、モナドを合成する必要を回避 しつつ、そのモナド計算を入れ子にすることができます。 Haskell においては、すべての計算は、トップレベルの IO モナド中で 起こります。それゆえ、これまで見てきたモナドの例はすべて、実際には 入れ子になったモナド計算の技法が使われているわけです。これを行う ために、計算は、開始部分において、そのすべての入力を行うことになります。 — 通常コマンドラインからの引数を読むことで行われます — そうしておいて、その値をモナド計算に渡し、結果を得ます。最後には、 計算の終端で出力を実行します。この構造は、モナド合成における問題は 回避していますが、これのおかげで例がときおり不自然なものに見えます。

例 18 で紹介されたコードはこの入れ子パターンでした。IO モナドのコマンド ラインから数字を読み、その数を Continuation モナド中の計算に渡して、文字 列を生成し、その後、その文字列を再び IO モナドに書き出しています。この IO モナド中の計算は、コマンドラインからの読み込みや文字列の書き出しに限 られているわけではなく、どこまでも複雑になりえます。さらに、内側の計算は どこまでも複雑になりえます。内側の計算が外側の機能に依存しないかぎりは それは外側のモナドの内部で安全に入れ子にすることができます。これは、 その値をコマンドライン引数のかわりに、標準入力からその値を読みこむという 例 18 のこのバリエーションで説明されています。

example19.hs で使えるコード
fun :: IO String
fun = do n <- (readLn::IO Int)         -- これは IO モナドブロック
         return $ (`runCont` id) $ do  -- これは Cont モナドブロック
           str <- callCC $ \exit1 -> do
             when (n < 10) (exit1 (show n))
             let ns = map digitToInt (show (n `div` 2))
             n' <- callCC $ \exit2 -> do
               when ((length ns) < 3) (exit2 (length ns))
               when ((length ns) < 5) (exit2 n)
               when ((length ns) < 7) $ do let ns' = map intToDigit (reverse ns)
                                           exit1 (dropWhile (=='0') ns')
               return $ sum ns
             return $ "(ns = " ++ (show ns) ++ ") " ++ (show n')
           return $ "Answer: " ++ str

合成されたモナド

もっと複雑な構造の計算についてはどうでしょう。もし入れ子になったパターン が使えないとすると、一つの計算のなかで2つ以上のモナドの属性を合成する 方法が必要です。そうするには、その値自身がもうひとつのモナド中のモナド値 であるようなモナド内の計算を実行します。たとえば、もし、Continuation モナドの計算内部で入出力を実行する必要がある場合、 型 Cont (IO String) a の Continuation モナド内の計算を実行 することになるでしょう。型 State (Either Err a) a のモナド を使って、単一の計算中で State モナドと Error モナドの機能を合成すること ができます。

始めの部分で同じ入出力を実行する例をすこし変更することを考えましょう。 ただし、Continuation モナドの計算の最中に追加の入力が必要になるでしょう。 この場合、入力値がある特定の範囲にあるときに出力部分をユーザに指定させる ことができるようにします。入出力は Continuation モナドの計算部分に依存 し、Continuation モナドの計算はその入出力の結果に依存しますので、 入れ子になったモナドのパターンが使えません。

その代りに Continuation モナドの計算に IO モナドからの値を使わせます。 従来、Int および String の値であったものが 今度は、IO Int および IO String となります。 IO モナドからは — 一方向モナドなので — 直接、値をとりだせ ませんので、Continuation モナド内部で IO モナドの do ブロックをネスト させて、値を操作する必要はほとんどありません。補助関数 toIO をつかって、Continuation モナド内部で入れ子になった IO モナドの値を生成 をハッキリさせます。

example20.hs で使えるコード
toIO :: a -> IO a
toIO x = return x

fun :: IO String
fun = do n <- (readLn::IO Int)         -- これは IO モナドブロック
         convert n
	 
convert :: Int -> IO String
convert n = (`runCont` id) $ do        -- これは Cont モナドブロック
              str <- callCC $ \exit1 -> do    -- str の型は IO String
                when (n < 10) (exit1 $ toIO (show n))
                let ns = map digitToInt (show (n `div` 2))
                n' <- callCC $ \exit2 -> do   -- n' has type IO Int
                  when ((length ns) < 3) (exit2 (toIO (length ns)))
                  when ((length ns) < 5) (exit2 $ do putStrLn "Enter a number:"
                                                     x <- (readLn::IO Int)
                                                     return x)
                  when ((length ns) < 7) $ do let ns' = map intToDigit (reverse ns)
                                              exit1 $ toIO (dropWhile (=='0') ns')
                  return (toIO (sum ns))
                return $ do num <- n'  -- これは IO モナドブロック
                            return $ "(ns = " ++ (show ns) ++ ") " ++ (show num)
              return $ do s <- str -- これは IO モナドブロック
                          return $ "Answer: " ++ s

これほで自明な例でさえも、異るモナドを同じ計算のなかで合成しようとすると わかりにくく醜いことになります。たしかに動きはしますが、美しくありません。 このコードをひとつひとつ比べると、手でモナドを合成するようなやり方では コードがどれほど汚くなるかが分ります。

例 19 からとった入れ子になったモナド 例 20 からとった手で合成したモナド
fun = do n <- (readLn::IO Int)
         return $ (`runCont` id) $ do
           str <- callCC $ \exit1 -> do
             when (n < 10) (exit1 (show n))
             let ns = map digitToInt (show (n `div` 2))
             n' <- callCC $ \exit2 -> do
               when ((length ns) < 3) (exit2 (length ns))
               when ((length ns) < 5) (exit2 n)
               when ((length ns) < 7) $ do
                 let ns' = map intToDigit (reverse ns)
                 exit1 (dropWhile (=='0') ns')
               return $ sum ns
             return $ "(ns = " ++ (show ns) ++ ") " ++ (show n')
           return $ "Answer: " ++ str
convert n = (`runCont` id) $ do
              str <- callCC $ \exit1 -> do
                when (n < 10) (exit1 $ toIO (show n))
                let ns = map digitToInt (show (n `div` 2))
                n' <- callCC $ \exit2 -> do
                  when ((length ns) < 3) (exit2 (toIO (length ns)))
                  when ((length ns) < 5) (exit2 $ do
                    putStrLn "Enter a number:"
                    x <- (readLn::IO Int)
                    return x)
                  when ((length ns) < 7) $ do
                    let ns' = map intToDigit (reverse ns)
                    exit1 $ toIO (dropWhile (=='0') ns')
                  return (toIO (sum ns))
                return $ do num <- n'
                            return $ "(ns = " ++ (show ns) ++ ") " ++ (show num)
              return $ do s <- str
                          return $ "Answer: " ++ s

Prev: Part III - イントロダクション TOC: 目次 Next: モナド変換子