プログラムにバグはつきものです。強力な型システムを備えている Haskell でもそれは同じです。この記事では、 Haskell プログラムのデバッグ手法をいくつか挙げてみます。
なお、使用している GHC は 8.2.2 です。より新しいバージョンで追加されるであろうより便利な機能は、この記事の対象外です。
【2018年2月8日 更新:-fexternal-interpreter, Control.Exception.assert, debug パッケージについて追記】
【2018年5月25日 更新:プロジェクトごとにPreludeを持っていると便利という話を追加】
目次
心構え:処理を分割せよ
Haskell は純粋な言語です。IOが絡まない関数であれば、同じ引数に対しては同じ結果が返ってくることが期待されます。
よって、処理を細かく(純粋な)関数に分割し、それぞれ GHCi で動作を確かめていけば、どこにバグが潜んでいるのか絞り込むことができます。
この際、関数の実行結果の型は Show クラスのインスタンスにしておくと便利です。
-- 良くないコード: main = do x <- ... ...でかい処理... print result
-- 良いコード: foo :: X -> Y foo x = ...ひとまとまり処理... bar :: Y -> Z bar y = ...ひとまとまりの処理... main = do x <- ... let result = bar (foo x) print result -- 機能を分割したことにより、 foo や bar を GHCi で個別にテストできる!
Haskell 版 printf デバッグ:Debug.Trace
C言語を始めとする手続き型プログラミング言語では、デバッグしたい箇所に printf などの命令を挟むことにより、「そのコードパスが実行されること」「その時点での各種変数の値」を確認することがあります。このような手法は俗に「printf デバッグ」と呼ばれます。
一方 Haskell では、純粋な関数においては基本的に、ログ出力のような副作用のある処理を実行できません。
しかしそれでは不便なので、 Debug.Trace というモジュールに「純粋な関数でログ出力できる関数」が(抜け道的に)用意されています:
-- Debug.Trace trace :: String -> a -> a traceShow :: Show a => a -> b -> b
他にもいくつかログ出力の関数が用意されています。ドキュメントを読むなり、 “Haskell Debug.Trace” でググるなりしてください。
(Debug.Trace はもちろん内部的には unsafePerformIO を使っていますが、 unsafePerformIO を直で使うよりも便利で安全だと思います。)
もちろん、 Haskell はデフォルトで遅延評価の言語ですし、 GHC は処理が純粋だと仮定して最適化するので、ログ出力の順番や回数は期待したものとは異なるかもしれません。
例えば、下記のコードを筆者の環境の GHC 8.2.2 で試したところ、 stack runghc
で実行した場合は
foo bar 1512
が、 stack ghc -- -O3
でコンパイルした場合は
bar foo 1512
が出力されました(i.e. 最適化によって出力の順番が違う)。
import Debug.Trace main = do let x = trace "foo" 42 let y = trace "bar" 36 print (x * y)
Control.Exception.assert
プログラムの特定の箇所で、期待する条件が満たされているかチェックしたい場合、普通は assert という機能を使うと思います。
普通のプログラミング言語では assert を文として記述するかと思いますが、純粋な(モナドに包まれていない) Haskell コード片では、式の評価時にチェックされるような assert を書くことになるでしょう。
もちろん、自前で次のような assert を定義することはできます:
assert :: Bool -> a -> a assert False _ = error "Assertion failed" assert True x = x
しかしこの程度の関数を自分で書く必要はなく、 GHC 標準(?)の assert が Control.Exception モジュールで提供されています。
Control.Exception モジュールで提供される assert の特徴は、
- コンパイルオプション
-fignore-assert
または最適化フラグ-O
が有効な場合に assert 呼び出しが消滅する(最適化モードで assert が消えて欲しくない場合は-fno-ignore-assert
を指定すれば良い) - 呼び出し箇所のファイル名と行番号を取得できる(後述する HasCallStack 制約に対応しているので。もちろん、自前の assert でも HasCallStack 制約をつければ可能)
点でしょうか。
プログラミング言語によっては、 assert 対象の式を文字列化してエラーメッセージに加えてくれるものがありますが、 Haskell の assert 関数は、関数である以上、そういう特殊なことはできません。(Template Haskell を使えば可能かもしれませんが、そういうライブラリーは軽くググった感じでは見当たりませんでした)
例:
import Control.Exception (assert) foo :: Int -> Int foo n = assert (n >= 0) $ n^2 bar :: Int -> Int bar n = foo (n^2 - 3 * n) main = print (bar 2)
対話的デバッグ: GHCi Debugger
GHCi にはデバッガーとしての機能が備わっています。
例えば、 :break 〈関数名〉
でその関数にブレークポイントを設置できます。 :step
でステップ実行ができます。詳しくはドキュメントを見てください。
欠点として、 GHCi のバイトコードインタープリターで実行できないコードに対しては適用できません。例えば、 unboxed tuples や unboxed sums を使っているとバイトコードコンパイルに失敗するようです。そのほか、高度な型レベルの機能に対しては対応が十分でないようです(例)。
参照:
- 5. Using GHCi — Glasgow Haskell Compiler 8.2.2 User’s Guide
- GHCiデバッガ – www.kotha.net 日本語訳
- GHCi debugger を使ってみた – khibino blog
バックトレースの取得:プロファイリング情報
コードの深いところでエラーが出ても、そのコード片がどこから呼ばれたのかわからないと困ります。いわゆるバックトレースが欲しくなります。
GHC では、プロファイリング情報を利用したバックトレースを取得できます。
プロファイリングを有効にして、バックトレースを取得するにはコンパイル時に -prof -fprof-auto
オプションをつけます。
例:
-- Test.hs foo :: Int -> Int foo n | n < 0 = error "negative!" | otherwise = n^2 bar :: Int -> Int bar n = foo (n^2 - 3 * n) main = print (bar 2)
stack ghc -- -prof -fprof-auto Test.hs
でコンパイルし、 ./Test
で実行します:
$ stack ghc -- -prof -fprof-auto Test.hs $ ./Test Test: negative! CallStack (from HasCallStack): error, called at Test.hs:2:17 in main:Main CallStack (from -prof): Main.foo (Test.hs:(2,1)-(3,23)) Main.bar (Test.hs:6:1-25) Main.main (Test.hs:8:1-20) Main.CAF (<entire-module>)
出力のうち、 CallStack (from -prof)
がプロファイリング情報によるバックトレースです。
GHCi でプロファイリング情報を有効にするには、 -fexternal-interpreter
を使う必要があるようです。例えば、 stack 経由であれば、次のように実行します:
$ stack repl Test.hs --ghci-options=-fexternal-interpreter --ghc-options=-prof --ghc-options=-fprof-auto
参照:
- GHC でスタックトレース – あどけない話
- 【Haskell】Debug.Traceとプロファイリングで幸せなデバッグ生活 – yu-i9.tmp
- Profiling call stacks — GHC.Stack
- Stack traces in GHCi, coming in GHC 8.0.1 · Simon Marlow
バックトレースの取得:HasCallStack 制約
GHC 8 では HasCallStack 制約という機能が追加されました。これは関数の型に制約として書くことができ、その関数内で使う error 呼び出しに関数の呼び出し元の情報を含めることができます。
「プロファイリング情報によるバックトレース」に比べた利点は、
- 特別なコンパイルオプションが要らない
- GHCi でも利用できる
あたりでしょうか。手軽なんです。
例を見てみましょう:
import GHC.Stack (HasCallStack) foo :: HasCallStack => Int -> Int foo n | n < 0 = error "negative!" -- error 関数に foo の呼び出し元の情報が渡される | otherwise = n^2 bar :: Int -> Int bar n = foo (n^2 - 3 * n) -- bar の情報が foo に渡される main = print (bar 2) -- main の情報は foo には渡らない
実行結果:
Test.hs: negative! CallStack (from HasCallStack): error, called at Test.hs:4:17 in main:Main foo, called at Test.hs:8:9 in main:Main
foo 関数に HasCallStack 制約をつけない場合は、 error が foo 関数(Test.hs の4行目)で発生したことしかわかりませんが、 foo 関数に HasCallStack 制約をつけた場合は、エラーメッセージに foo の呼び出し元の情報(Test.hs の 8 行目;つまり bar 関数)も含まれるようになります。
制限として、 HasCallStack 制約を持たない関数があるとそこでバックトレースが途切れます。例では、関数 bar は HasCallStack 制約を持たないので、「foo の呼び出し元が bar である」ことはわかっても、「bar の呼び出し元が誰なのか」はわかりません。
(Haskell に詳しい人は関数の型クラス制約は詰まるところ暗黙の引数なんだということはご存知でしょう。 HasCallStack 制約も暗黙の引数の一種であって、型レベルのナニカ、あるいはオブジェクトコードのデバッグ情報的なナニカではなく、単に呼び出し元の情報をデータとして渡しているだけのようです。そう考えれば、 HasCallStack 制約が途切れるとバックトレースが途切れるという動作にも納得がいくでしょう。)
なお、トップレベル関数の型注釈を省略した場合、通常の型クラス制約は型推論で補われますが、 HasCallStack 制約は補われません。HasCallStack 制約を書きましょう。
参照:
- 10.1. Language options — Glasgow Haskell Compiler 8.2.2 User’s Guide
- HasCallStack call stacks — GHC.Stack
小ネタ:プロジェクトごとにPreludeを持っているとHasCallStackデバッグに便利
最近は代替Preludeを使うことも一般的になってきましたね[要出典]。(参考記事:Prelude を カスタムPrelude で置き換える)
ここではbaseかrioかというのはどうでもよくて、プロジェクトごとにPreludeの役割をするモジュールを持っておくと便利という話をします。
例えば、エラー発生箇所がPreludeの
head :: [a] -> a
で、head関数の呼び出し元をHasCallStackで突き止めたいとします(この話はPreludeのエラーを投げる関数ならどれでも当てはまります。筆者が実際に遭遇したのは (^) :: (Num a, Integral b) => a -> b -> a
でした。部分関数を排除した代替Preludeを使っている方には関係ないかもしれません)。
困ったことにheadはPreludeの関数なので、ソースをいじってHasCallStack制約をつけるということができません。head関数を独自に
import Prelude hiding (head) import GHC.Stack (HasCallStack) head :: HasCallStack => [a] -> a head [] = error "empty list" head (x:_) = x
と定義すればHasCallStackデバッグができますが、プロジェクトに含まれるたくさんのモジュール全てに import Prelude hiding (head)
を書き足すのは大変です。
そんな時、プロジェクト独自のPrelude(ここではMyPreludeというモジュール名とします)を
module MyPrelude (module Prelude) where import Prelude
と定義しておけば、head関数をHasCallStackデバッグしたくなったらMyPreludeを
module MyPrelude (module Prelude,head) where import Prelude hiding (head) import GHC.Stack head :: HasCallStack => [a] -> a head [] = error "empty list" head (x:_) = x
といじるだけでhead関数に対するHasCallStackデバッグができるようになります。
このテクニックはHasCallStackの利用以外にも応用できそうです。
実行トレース:debug パッケージ
山本悠滋氏にコメントで教えていただきましたが、最近 debug パッケージというものが登場したようです。
準備として、デバッグしたい関数の定義を debug [d|
… |]
で囲みます。
デバッグ実行の手順としては、まず普通に GHCi で関数を実行します。その後に(debug パッケージの提供する) debugView 関数を呼ぶと、ブラウザーが立ち上がり、関数呼び出しにおける実際の引数、ローカル変数、返り値が確認できるという寸法です。
例:
{-# LANGUAGE TemplateHaskell, ViewPatterns, PartialTypeSignatures #-} import Debug debug [d| foo :: Int -> Int foo n = n^2 + n_plus_1 where n_plus_1 = n + 1 n_plus_2 = n + 2 bar :: Int -> Int bar n = foo (n^2 - 3 * n) main = print (bar 2) |]
GHCi で次のように実行:
*Main> main --> まず普通に実行する 3 *Main> debugView --> ブラウザーが立ち上がる
すると次のような画面が表示されます:
注意点として、実際に評価されない変数(この例だと n_plus_2
)の値は確認できません。そのほか、 値を表示するためには値が Show のインスタンスである必要があります。
また、途中でエラーが発生する場合には使えません。
いちいち Template Haskell を有効にしたり debug [d| |]
で囲ったりするのが面倒だ、という人のために、プリプロセッサーが用意されているようです。詳しくはマニュアルを見てください。
- https://github.com/ndmitchell/debug
- https://hackage.haskell.org/package/debug
- Neil Mitchell’s Haskell Blog: Announcing the ‘debug’ package
その他
Debugging – HaskellWiki もこの記事と同様の趣旨のページで、ここで取り上げていないツールもいくつか紹介されているようです。
実践
一般論はここまでです。ここからは、筆者が今日行ったデバッグの話をします。興味がなかったら読み飛ばしていただいて結構です。
今日のデバッグ対象は、 arithmoi パッケージの Math.NumberTheory.Primes.sieveFrom 関数です。
この関数は sieveFrom :: Integer -> [Integer]
という型を持ち、与えられた引数以上の素数をリストで返します。例えば、
> take 10 $ sieveFrom 100 [101,103,107,109,113,127,131,137,139,149]
という具合です。
ただし、あまりに大きい整数を与えると Out of memory が出ます:
> take 10 $ sieveFrom (10^48) <interactive>: Out of memory
まあ、 Out of memory が出るのは仕様です。ドキュメントにもそういうことが書かれています。
しかし…
> take 10 $ sieveFrom 5801977483380505537128388351953140502138565165057 *** Exception: integerSquareRoot: negative argument
期待される動作は Out of memory ですが、これは…?
integerSquareRoot :: Integral a => a -> a
は同じ arithmoi の Math.NumberTheory.Powers.Squares モジュールで定義されている関数で、ソースを見ればわかりますが、負の整数を与えるとエラーを吐きます。
可能性としては、 sieveFrom で実行されるコード中に Int を使っている箇所があって、そこでオーバーフローを起こしていることが考えられます。突き止めてみましょう。
準備
arithmoi のソースを手元に持ってきて、 stack repl で実行できるようにします。
$ git clone https://github.com/cartazio/arithmoi.git $ cd arithmoi $ stack init $ stack repl
行動1. integerSquareRoot にブレークポイントを仕掛ける
GHCi のデバッグ機能を使って、 integerSquareRoot にブレークポイントを設定しましょう。…と言いたいところですが、 arithmoi パッケージには unboxed tuple を使っている箇所があるため、 GHCi で読み込めません!
[17 of 55] Compiling Math.NumberTheory.Utils ( **/arithmoi/Math/NumberTheory/Utils.hs, interpreted ) Error: bytecode compiler can't handle unboxed tuples and sums. Possibly due to foreign import/export decls in source. Workaround: use -fobject-code, or compile this module to .o separately.
Workaround に示されているように -fobject-code
オプションを使うと REPL は起動しますが、肝心のデバッグができません。
当該モジュールを別途コンパイルすると回避できるとか言ってますが、面倒くさそうです。unboxed tuple を使わないようにコードを書き換えるという手も考えましたが、やはり面倒くさそうです。
よって、 GHCi のデバッグ機能は諦めます。
以降、 REPL は stack repl --ghc-options -fobject-code
で起動します。
sieveFrom
という名前の関数は複数のモジュールで定義されているため、 GHCi の :m Math.NumberTheory.Primes
コマンドにより曖昧さをなくします。
$ stack repl --ghc-options -fobject-code ....... .......> :m Math.NumberTheory.Primes Prelude Math.NumberTheory.Primes> take 10 $ sieveFrom 5801977483380505537128388351953140502138565165057 *** Exception: integerSquareRoot: negative argument CallStack (from HasCallStack): error, called at **/arithmoi/Math/NumberTheory/Powers/Squares.hs:50:19 in main:Math.NumberTheory.Powers.Squares
行動2. integerSquareRoot に HasCallStack を仕掛ける
なんとかして integerSquareRoot の呼び出し元を調べたいものです。
GHCi で試す都合上、プロファイリング情報によるバックトレースは使えません。仕方がないので、 HasCallStack 制約によるバックトレースを使います。
integerSquareRoot は Math/NumberTheory/Powers/Squares.hs に
integerSquareRoot :: Integral a => a -> a integerSquareRoot n | n < 0 = error "integerSquareRoot: negative argument" | otherwise = integerSquareRoot' n
として定義されています。このファイルの先頭の方に import GHC.Stack (HasCallStack)
を追加し、 integerSquareRoot の型を integerSquareRoot :: (HasCallStack, Integral a) => a -> a
に変えます。
変更を保存したら、 GHCi で :r
を叩き、再読み込みします。もう一度同じコード片を実行すると
Prelude Math.NumberTheory.Primes> :r ... Prelude Math.NumberTheory.Primes Math.NumberTheory.Zeta> take 10 $ sieveFrom 5801977483380505537128388351953140502138565165057 *** Exception: integerSquareRoot: negative argument CallStack (from HasCallStack): error, called at **/arithmoi/Math/NumberTheory/Powers/Squares.hs:50:19 in main:Math.NumberTheory.Powers.Squares integerSquareRoot, called at **/arithmoi/Math/NumberTheory/Primes/Sieve/Eratosthenes.hs:222:14 in main:Math.NumberTheory.Primes.Sieve.Eratosthenes
と、バックトレースが増えました。問題となる integerSquareRoot の呼び出し元は Math/NumberTheory/Primes/Sieve/Eratosthenes.hs の222行目のようです。当該部分を見ると
sieveTo :: Integer -> ST s (STUArray s Int Bool) sieveTo bound = arr where (bytes,lidx) = idxPr bound !mxidx = 8*bytes+lidx mxval :: Integer mxval = 30*fromIntegral bytes + fromIntegral (rho lidx) !mxsve = integerSquareRoot mxval ----------------- ← 222行目! (kr,r) = idxPr mxsve !svbd = 8*kr+r
となっているので、 mxval が負になっていると予想できます。
行動3. Debug.Trace
mxval の定義から、 bytes :: Int
または rho lidx :: Int
がオーバーフローして負になっていると考えられます。関数の引数である bound
もチェックしておきたいところです。
先に説明したように、今回 GHCi のデバッグ機能は使えないので、 Debug.Trace を利用します。
Erathosthenes.hs の先頭の方に import Debug.Trace
を追加し、mxsve の定義に traceShow を仕込みます:
!mxsve = traceShow (bound, bytes, rho lidx) $ integerSquareRoot mxval
複数の変数を show するのが面倒なので、タプルにして traceShow を使っています。
再度 GHCi で :r
を叩き、問題のコードを実行します:
Prelude Math.NumberTheory.Primes Math.NumberTheory.Zeta> :r [29 of 55] Compiling Math.NumberTheory.Primes.Sieve.Eratosthenes ( **/arithmoi/Math/NumberTheory/Primes/Sieve/Eratosthenes.hs, **/arithmoi/.stack-work/odir/Math/NumberTheory/Primes/Sieve/Eratosthenes.o ) Ok, 55 modules loaded. Prelude Math.NumberTheory.Primes Math.NumberTheory.Zeta> take 10 $ sieveFrom 5801977483380505537128388351953140502138565165057 (2408729433411005906443201,-7695839157481303009,31) *** Exception: integerSquareRoot: negative argument CallStack (from HasCallStack): error, called at **/arithmoi/Math/NumberTheory/Powers/Squares.hs:50:19 in main:Math.NumberTheory.Powers.Squares integerSquareRoot, called at **/arithmoi/Math/NumberTheory/Primes/Sieve/Eratosthenes.hs:222:51 in main:Math.NumberTheory.Primes.Sieve.Eratosthenes
traceShow の結果は (2408729433411005906443201,-7695839157481303009,31)
で、要するに bytes
がオーバーフローにより負になっていることがわかりました。
あとは bytes
変数の由来である idxPtr
関数を調べるなりできそうです。しかし配列のインデックスに Int
を使う以上、この手のオーバーフローは避けられなさそうです。どうするのがいいのか(もっとわかりやすいエラーを出すべき?)筆者にはわかりません。
ともあれ、 Haskell におけるデバッグの良い演習にはなったと思います。
最近作られた https://hackage.haskell.org/package/debug も試してみてください!
個人的に試したことはないのですが、ドキュメント読む限りはシンプルで便利そうです!