あどけない話

Internet technologies

Haskell でのデバッグ

「純粋関数型言語デバッグしにくい。だって純粋な関数で printf デバッグできないから」とつぶやいている人をよく見かけます。これまで放置してきましたが、リツイートが50を超えたので、Haskellでのデバッグについて書きます。

例外処理と同じように、Haskell でのデバッグでは、純粋な関数と IO を分けて考える必要あります。

IO での printf デバッグ

IO では、putStrLn や print が使えるから問題ないですよね?

foo :: Int -> IO Bool
foo i = do
    x <- あれして i
    putStrLn $ "x = " ++ show x
    これして
    putStrLn "ここも通過"
    -- それもする
    y <- それもする
    print y
    return y

ちなみに、forkIO 起動した軽量スレッドから putStrLn する場合、軽量スレッドがたくさんいると標準出力/エラーへの出力がごちゃごちゃにならないか心配する人もいるかもしれません。標準出力/エラーは、MVar で守られているので、奇麗に出力されますから、ご心配なく。

例外のデバッグ

例外を補足できるのは IO の中でのみです。どういう例外が起こったか知りたい場合は catch に渡すハンドラの中で例外を表示しましょう。forkIO に渡す IO や main には、ハンドラを付けておくことをお勧めします。

import Control.Concurrent
import Control.Exception as E

errorHandler :: SomeException -> IO ()
errorHandler = print

main :: IO ()
main = handle errorHandler $ do
    あれして
    forkIO (軽量スレッド `E.catch` errorHandler)
    これして
    それもする

端末を切り離すデーモンにする場合は、標準出力/エラーは使えなくなりますから、ファイルに書き出すのがお勧めです。

errorHandler :: ErrorCall -> IO ()
errorHandler = appendFile "/tmp/debug.log" . show

あと、例外がどこから来たのか知りたい場合は、GHC のデバッガを使うといいでしょう。(追記:GHC でスタックトレースも読んで下さい。)

純粋な関数での printf デバッグ

さてさて、純粋な関数での printf デバッグです。そんなこと、できるんでしょうか?

できるんです。そう、Debug.Trace の trace を使えばね。

trace の第一引数はデバッグ用の文字列、第二引数は trace を仕込む式が返すべき値です。

trace :: String -> a -> a

例として連想リストを線形探索するプログラムを考えます。

search :: Eq k => k -> [(k,v)] -> Maybe v
search _ [] = Nothing
search k ((xk,xv):xs)
  | k == xk   = Just xv
  | otherwise = search k xs

trace を仕込むと、以下のようになります。Show の制約が増えていることに注意。

-- search :: Eq k => k -> [(k,v)] -> Maybe v
search :: (Eq k, Show k, Show v) => k -> [(k,v)] -> Maybe v
search _ [] = Nothing
search k ((xk,xv):xs)
  | k == xk   = Just xv
--  | otherwise = search k xs
  | otherwise = trace ("(k,v) = " ++ show (xk,xv)) (search k xs)

使ってみましょう。

> search 3 [(4,'a'),(5,'b'),(3,'c'),(3,'d'),(2,'e')]
(k,v) = (4,'a')
(k,v) = (5,'b')
Just 'c'

途中経過が表示されましたね。

スマートリテラル

純粋な関数はデバッグしにくいどころか、デバッグし易いんですよ。だって、副作用がないですからね。GHCi で対話的にデバッグしちゃいましょう。Haskell で対話的にデバッグし易いもう一つの理由は、Haskell にはスマートリテラル(造語ですよ)があるからなんです。

僕の言うスマートリテラルとは、データ定義がそのままリテラルになって、入力にも使えるし、そのままの形で出力されるデータのことです。

たとえば、次のような木構造を定義します。

data Tree a = Leaf | Node (Tree a) a (Tree a) deriving Show

そして以下のような木をどう表現するか、考えてみましょう。

  2
 / \
1   3

Haskellでは、木を生成するコードなど一切不要で、以下のようなリテラルで表現できます。

Node (Node Leaf 1 Leaf) 2 (Node Leaf 3 Leaf)

つまり、print すればこのように表示されるし、それをコピー&ペーストして関数の引数に与えられるということです。

print で表示できないのは、関数か Show のインスタンスになってないデータです。後者に関しては、Show のインスタンスにする方法が提供されています。たとえば、上記の木が Show のインスタンスではないとしましょう。

data Tree a = Leaf | Node (Tree a) a (Tree a)

そして、このコードに derving Show を足すのは難しい状況だとします。その場合、この木を使うプログラムに以下のようなコードを書けば Show のインスタンスとして利用できるようになります。

{-# LANGUAGE StandaloneDeriving #-}

deriving instance Show a => Show (Tree a)

おまけ

Haskellでは、printfデバッグするとプログラムの意味を変えてしまうことがあります。遅延していた計算を、表示するために計算してしまうからです。でも、どうせデバッグ中なんですから、問題になることはないでしょう。