純粋関数でのトレース

 次にHaskellでトレースを行う方法を考えましょう。

 トレース対象のコードがI/Oアクションなら話は簡単です。単に,Showクラスのインスタンスになっている型をprintで出力するだけで済みます。あるいは,showメソッドを使って作成した文字列を別の関数に渡しても構いません。いずれにせよ,I/Oアクションの中でなら好きなところにトレース用の処理を挟み込むことができます。

 問題なのは,トレースしたい対象がI/Oアクションでない場合です。これは二つの場合に分けられます。対象のコードがモナドである(モナド則が成り立つものである)場合と,モナドでない場合です。

 モナドではない場合は,手動でトレースを行うのはかなり難しくなります。まずは胸に手を当てて,本当にトレースが必要かどうかもう一度考えてください。

 トレースが必要になるのは,ある処理の間に保持されている現在の状態を知りたい場合です。ところが,Haskellでは変化する状態が保持される場所は,きわめて限定されます。I/Oモナドで行われる参照型の操作や,Stateモナドを使った状態のエミュレートなど,モナドを使用している部分に限られます。モナド以外では状態はなく,関数の振る舞いは,純粋に入力値を受け取った結果としての値に固定されます。それなら,わざわざ現在の変数の値を表示させなくても,対話環境などを通じて入力に対する出力をテストするだけでこと足りるのではないでしょうか?

 このように,Haskellではトレースが最良の解決方法ではないことがよくあります。モナド以外の関数をトレースしたくなったら,まずは関数単体あるいは複数の関数の組み合わせに対するユニット・テスト(unit testing)では不十分なのかを考えてみてください(参考リンク)。

 それでも関数の振る舞いを追いたい場合,Haskellの処理系には二つの手段が用意されています。

 一つは,デバッガです。HugsではHoodというモジュール,GHCではGHC6.8.1からGHCi Debuggerという機能がデバッガとして提供されています。これらを使えば関数の振る舞いを追うことができます(参考リンク1参考リンク2)。ただ,今回はこれらについて特に説明しません。興味のある方はマニュアルを参照してください。

 もう一つ,純粋な関数内で手動のトレースを行うための機能があります。Debug.Traceモジュールです。今回は,:browseコマンドを使ってDebug.Traceモジュール中の全ての構成要素を一度に表示させてみましょう(参考リンク1参考リンク2)。

Prelude Debug.Trace> :browse Debug.Trace
putTraceMsg :: String -> IO ()
trace :: String -> a -> a
traceShow :: (Show a) => a -> b -> b

 traceは第2引数に対する恒等関数(id)ですが,式の評価時に第1引数として与えたStringを表示するという副作用を持ちます。traceShow関数はtrace関数を一般化したもので,第1引数として与えたShowクラスのインスタンスの型を表示します(ただし,開発者のミスによりGHC6.8.1ではまだこの関数はエクスポートされていません。GHCを使う場合には,自分で定義するか,次のメジャー・バージョンアップを待ってください。参考リンク)。

traceShow = trace . show

 副作用を伴った処理なので,traceは実装にunsafePerformIOを利用しています。しかし,その出力に利用しているのは,これまで紹介したprintやputStrではありません。

trace :: String -> a -> a
trace string expr = unsafePerformIO $ do
    putTraceMsg string
    return expr

 同じモジュールのputTraceMsgです。なぜでしょうか?

 これには二つ理由があります。一つは,文字列を標準出力ではなく標準エラーに出力するためです。もう一つは,GHCを使ってWindowsでGUIアプリケーションを作成したときに,存在しない可能性があるコマンド プロンプトではなく,デバッグ用のコンソールに出力するためです(参考リンク1参考リンク2参考リンク3)。

 実際にDebug.Traceを使ってみましょう。

quicksort'  []           =  []
quicksort' (x:xs)        =  quicksort' [y | y <- xs, y<x ]
                        ++ [x]
                        ++ quicksort' [y | y <- xs, y>=x]

 プログラムの構造にもよりますが,Debug.Traceを使ってわかりやすい出力を得るにはそれなりの改造が必要です。

quicksort []     = []
quicksort (x:xs) = trace ("x." ++ show x) $
                   trace ("x." ++ show x ++ " > y." ++ show losort) $
                   trace ("x." ++ show x ++ " <= y." ++ show hisort) $
                   losort ++ [x] ++ hisort
                   where
                     losort = quicksort [y|y <- xs, y < x] 
                     hisort = quicksort [y|y <- xs, y >= x]

 quicksort関数の適用を評価した結果は以下の通りです。

*Trace> quicksort [43,53,12,1,157,754]
x.43
x.12
x.1
x.1 > y.[]
x.1 <= y.[]
x.12 > y.[1]
x.12 <= y.[]
x.43 > y.[1,12]
x.53
x.53 > y.[]
x.157
x.157 > y.[]
x.754
x.754 > y.[]
x.754 <= y.[]
x.157 <= y.[754]
x.53 <= y.[157,754]
x.43 <= y.[53,157,754]
[1,12,43,53,157,754]
*Trace> quicksort [5,4..1]
x.5
x.4
x.3
x.2
x.1
x.1 > y.[]
x.1 <= y.[]
x.2 > y.[1]
x.2 <= y.[]
x.3 > y.[1,2]
x.3 <= y.[]
x.4 > y.[1,2,3]
x.4 <= y.[]
x.5 > y.[1,2,3,4]
x.5 <= y.[]
[1,2,3,4,5]

 どのようにリストを整列しているのかが出力からだいたいわかると思います。

 このように,Debug.Traceを使うことで純粋関数であってもプログラムの挙動をトレースできるようになります(参考リンク1参考リンク2)。

 ただし,traceやtraceShowはunsafePerformIOを使って実装された関数であるため,値が評価されない場合には表示用の処理も行われないという欠点があります(参考リンク)。また,関数によってはなかなか思い通りに結果が表示されず,使いにくいと感じることもあるでしょう。デバッガが使えるなら,Debug.Traceよりそちらを使ったほうがいい場合もあります。

モナド変換子とは?

 モナドの場合,Debug.Traceを使わなくても「モナド変換子」を用いることでトレースができます。まず,モナド変換子について説明しましょう。

 モナド変換子とは,あるモナドmを取りt mというモナドを生成するような型構成子tのことです。例えば,tがモナドにStateモナドとしての状態を保持する能力を追加するStateT s m a型のモナド変換子である場合,mがListモナドなら,StateT s [] aはListモナドとStateモナドの能力を兼ね備えた新しいモナドになります。このようにモナドを別のモナドに変換するので,モナド変換子と呼ぶのです。

 実はモナド変換子という言葉は以前の回で既に登場しています。第6回で「getとsetをStateモナドだけではなくモナド変換子でも利用できるよう,MonadStateという型クラスのメソッドとして宣言している」と説明しました。また,Stateモナド等のモナドを提供しているライブラリのパッケージ名がmtl(A monad transformer library)であるとも述べました。実は,mtlは単にモナドを提供するライブラリというだけではなく,モナド変換子を提供することを目的としたライブラリだったのです。

 mtlでは,Control.Monad.Stateモジュールのモナド変換子StateTは以下のように定義されています。

newtype StateT s m a = StateT { runStateT :: s -> m (a,s) }

newtype State s a = State { runState :: s -> (a, s) }

 StateTはStateとは異なり,モナドmを型変数として取り,結果をモナドmに包まれた形で返します。

 この結果,StateTモナド変換子から生成されたモナドの定義は,モナドm内で行われる計算になります。

instance (Monad m) => Functor (StateT s m) where
    fmap f m = StateT $ \s -> do
        ~(x, s') <- runStateT m s
        return (f x, s')

instance (Monad m) => Monad (StateT s m) where
    return a = StateT $ \s -> return (a, s)
    m >>= k  = StateT $ \s -> do
        ~(a, s') <- runStateT m s
        runStateT (k a) s'

instance (Monad m) => MonadState s (StateT s m) where
    get   = StateT $ \s -> return (s, s)
    put s = StateT $ \_ -> return ((), s)

 型定義の上ではt mのように内側に書かれるモナドmが,計算上では外側に来る点に注意してください。

instance (Monad m) => Functor (StateT s m) where
    fmap f m = StateT $ \s -> do
        ~(x, s') <- runStateT m s
        return (f x, s')

instance (Monad m) => Monad (StateT s m) where
    return a = StateT $ \s -> return (a, s)
    m >>= k  = StateT $ \s -> do
        ~(a, s') <- runStateT m s
        runStateT (k a) s'

 第2回で説明したように,GHCiではプロンプトでの処理をI/Oアクションとして実行します(参考リンク)。このため,単純な例ではmをIOとみなして結果を出力します。

Prelude Control.Monad.State> runStateT (get >>= \x -> return (x + 1)) 22
(23,22)

 ただ実際には,StateモナドとStateT s mモナドの結果は違う型です。

Prelude Control.Monad.State> runState (get >>= \x -> return (x + 1)) 22
(23,22)
Prelude Control.Monad.State> :t runState (get >>= \x -> return (x + 1)) 22
runState (get >>= \x -> return (x + 1)) 22 :: (Num s) => (s, s)
Prelude Control.Monad.State> runStateT (get >>= \x -> return (x + 1)) 22
(23,22)
Prelude Control.Monad.State> :t runStateT (get >>= \x -> return (x + 1)) 22
runStateT (get >>= \x -> return (x + 1)) 22 :: (MonadState s (StateT s m), Num s) => m (s, s)

 このため,Stateモナドを単純にStateTに置き換えることはできません。多くの場合,型が一致しないというエラーが生じるでしょう。StateTをStateモナドと同じように利用するには,どうすればいいでしょうか?

 mtlでは解決策として,Control.Monad.Identityモジュールで「恒等(Identity)モナド」を提供しています。恒等モナドはその名の通り,id::a -> aのような恒等式を表すものです。なので,恒等モナドでは通常の関数適用以外の特別な処理は何も行いません。

newtype Identity a = Identity { runIdentity :: a }

instance Functor Identity where
    fmap f m = Identity (f (runIdentity m))

instance Monad Identity where
    return a = Identity a
    m >>= k  = k (runIdentity m)

 この結果,モナド変換子を恒等モナドに適用すると,そのモナド変換子の能力を持ったモナドそのものになります。つまり,StateT s Identity aはStateモナドとほぼ同じものになるのです(参考リンク)。これで問題なく,必要に応じてコード中のモナド変換子をモナドに置き換えられます。

Prelude Control.Monad.State Control.Monad.Identity> runIdentity $ runStateT (get >>= \x -> return (x + 1)) 22
(23,22)
Prelude Control.Monad.State Control.Monad.Identity> :t runIdentity $ runStateT (get >>= \x -> return (x + 1)) 22
runIdentity $ runStateT (get >>= \x -> return (x + 1)) 22 :: (MonadState s (StateT s Identity), Num s) => (s, s)

 モナド変換子の型の都合上,先ほどの例と同様に,モナド変換子の適用とフィールド・ラベルを使った値へのアクセスが逆方向になっている点に注意してください。一番外側に来るモナドの値を一番外側のフィールド・ラベルで取り出しているので,そう混乱することはないと思います。