読者です 読者をやめる 読者になる 読者になる

Haskell入門者*1のデバッグ3つ道具

(この記事はHaskell Advent Calendar jp 2010のために書かれました)

要旨

絶対にバグが出ない言語などというものは
無く、理不尽な挙動を前にして
途方にくれる時もある。
それを乗り越える為には、確固
たる信念と洞察、そして
(1)deriving Show
(2)print
(3)Debug.Trace
が必要である。

Haskell

Haskellはやっぱり魅力的です。無限リストを使ったプログラム(速度はともかく)や、自分でも理解しないまま型を合わせただけのプログラムがなぜか無事に動いたりすると魔法すら感じられます。ソースの見た目も美しいです。シンプルに書けて副作用が無くて型推論が有るので、Haskellはバグの出にくい言語とされています。

それだけに、Haskellで出たバグは取りにくく、しかも苦労して突き止めた結果が言語設計の根幹に関わる深刻な問題とかならいいんですが僕の書くようなレベルでそういうことはそうそうなく、たいていは苦労に見合わないがっかりミスなわけで。

うっかり変数名が重複していて、なんでも無いはずの関数が停止しない無限再帰になってたり、なんでも無いはずのリストが無限リストになってたりするミス、ってのがけっこうありました。

デバッグ

こういうミスがあると、あるところでプログラムが無限(bottom)を評価しようとし、フリーズします。そうなったら僕はどうするかというと、まず(0)やっぱこれHaskellで書くのやめようかな、と思いますが、書きかかったものを捨てるのも勿体ないのでなんとかデバッグしないといけません。

まずめぼしいクラスを(1)deriving Showします。プログラムはたいていIOモナドなので、なんでも表示できる(2)print :: Show a => a -> IO ()を色んな所に追加して、プログラムの流れを追って行くとどこでフリーズしたのかがわかります。

えてしてフリーズした箇所は、無限(bottom)を評価した箇所であって、その無限を生成した箇所ではありません。爆弾(bottom)が仕込まれた時点を突き止めなければなりません。そのためには(3)Debug.Traceを使って、こんどはIOじゃない、普通の計算をやっている部分を遡っていきます。僕は以下のような小さなデバッグ用のライブラリを作っていつも使っています。

import qualified Debug.Trace as D

debugMode = True

trace::String -> a -> a
trace = if debugMode then D.trace else flip const

watch::Show a => a -> b -> b
watch a b = trace (show a) b

spy::Show a => a -> a
spy a = trace (show a) a

spyの型がid :: a -> aとほぼ同じなので、純粋なプログラムのどこにでも挟むことができます。spyを値が通過したとき、その値が表示されます。

まとめ

Haskellのバグは、えてしてバグを仕込んだ所と出現する所が離れている、凶悪なバグなので困ります。
ついでに、関数を含む型はderiving Showできないのも困ります。
しかしそいつは身勝手というもの。

spyを仕込んでいると、思いもかけない順番でトレースが出てきたり、出てくるはずのトレースが出て来なかったりして、デバッグ作業自身がサブ・パズルになってきます。そのパズルが解けるたびに遅延評価がすこしづつ分かった気になるので楽しいです。


今日のお話は、だいたいshelarcyさんの連載から学んだものです。ありがとうございました。