GHCi debugger を使ってみた
Haskell Advent Calendar 2011 のためのエントリです。
最近、会社でも Haskell を開発のメンバーで使っていくことに正式に決まりました。
Haskell のプログラムをデバッグするときにデバッガーのようなツールを使うことが
できるのか社内で質問されたので調べてみました。
GHCi debugger
公式のドキュメント
http://www.haskell.org/ghc/docs/7.0.4/html/users_guide/ghci-debugger.html
や
その翻訳
http://www.kotha.net/ghcguide_ja/7.0.4/ghci-debugger.htmlを
参考にしながら試してみて、私なりに理解した内容を紹介しようと思います。 mkothaさん、すばらしい翻訳をありがとうございます。
さっそく、GHCi debugger を使ってみましょう。
GHCi debugger は対話環境である GHCi に組込まれている debugger です。
まずプログラムを GHCi に読み込みます。
例えば、以下のような階乗を計算するプログラムを例にとってみましょう。
factorial.hs
factorial 0 = 1 factorial n = n * factorial (n - 1) main = print (factorial 20)
% ghci factorial.hs GHCi, version 7.0.4: http://www.haskell.org/ghc/ :? for help Loading package ghc-prim ... linking ... done. Loading package integer-gmp ... linking ... done. Loading package base ... linking ... done. [1 of 1] Compiling Main ( factorial.hs, interpreted ) Ok, modules loaded: Main. *Main>
ブレークポイント - :break
ブレークポイントを設定してみます。
ブレークポイントを設定するには GHCi のコマンドである :break を使います。
コマンドは短く省略できるようになっているらしく、:b でも良いようです。
引数に関数名を指定しています。
:show breaks で、設定したブレークポイントを確認することができます。
ブレークポイントには番号が振られるようです。ここでは 0 ですね。
*Main> :b factorial Breakpoint 0 activated at factorial.hs:(1,1)-(2,35) *Main> :break factorial Breakpoint 0 was already set at factorial.hs:(1,1)-(2,35) *Main> :show breaks [0] Main factorial.hs:(1,1)-(2,35) *Main>
以下の様に実行してみるとブレークポイントで止まります。
止まった箇所のソースコードを :list で確認することができます。
vv と ^^ で次に実行する箇所を範囲で示しているようです。
factorial をブレークポイントに設定したので、
呼び出しの直前で停止しています。
*Main> main Stopped at factorial.hs:(1,1)-(2,35) _result :: a = _ [factorial.hs:(1,1)-(2,35)] *Main> :list vv 1 factorial 0 = 1 2 factorial n = n * factorial (n - 1) ^^ 3 [factorial.hs:(1,1)-(2,35)] *Main>
ステップ実行と自由変数 - :step, :print
これだけではおもしろくもないので、
関数の中へステップ実行してみることにします。
今度は ^^^ で次の実行箇所が示されています。
ここでは自由変数 n があるのでその値を覗き見することができます。
:step の直後に
n :: Integer = 20
と表示されているのがそうです。
[factorial.hs:(1,1)-(2,35)] *Main> :step Stopped at factorial.hs:2:15-35 _result :: Integer = _ n :: Integer = 20 [factorial.hs:2:15-35] *Main> :list 1 factorial 0 = 1 2 factorial n = n * factorial (n - 1) ^^^^^^^^^^^^^^^^^^^^^ 3 [factorial.hs:2:15-35] *Main>
明示的に表示させる :print というコマンドもあります。
[factorial.hs:2:15-35] *Main> :print n n = 20 [factorial.hs:2:15-35] *Main> :p n n = 20 [factorial.hs:2:15-35] *Main>
:set stop コマンドで
止まったときに実行するコマンドを設定することもできます。
ここでは :list を実行するようにしてみましょう。
そして、いくつかステップを進めてみます。
[factorial.hs:2:15-35] *Main> :set stop :list [factorial.hs:2:15-35] *Main> :st Stopped at factorial.hs:2:19-35 _result :: Integer = _ n :: Integer = 20 1 factorial 0 = 1 2 factorial n = n * factorial (n - 1) ^^^^^^^^^^^^^^^^^ 3 [factorial.hs:2:19-35] *Main> :st Stopped at factorial.hs:(1,1)-(2,35) _result :: a = _ vv 1 factorial 0 = 1 2 factorial n = n * factorial (n - 1) ^^ 3 [factorial.hs:(1,1)-(2,35)] *Main> :st Stopped at factorial.hs:2:30-34 _result :: Integer = _ n :: Integer = 20 1 factorial 0 = 1 2 factorial n = n * factorial (n - 1) ^^^^^ 3 [factorial.hs:2:30-34] *Main> :st Stopped at factorial.hs:2:15-35 _result :: Integer = _ n :: Integer = 19 1 factorial 0 = 1 2 factorial n = n * factorial (n - 1) ^^^^^^^^^^^^^^^^^^^^^ 3 [factorial.hs:2:15-35] *Main>
1つ少ない n があるところまでテップすることができました。
履歴 - :history
GHCi debugger にはスタックトレースを参照する機能は無いのですが、
代わりにバックステップを行なうことができます。
:back コマンドを使うことで、記録されている履歴へ戻ることができます。
記録されている履歴の一覧を :history コマンドで確認することができます。
履歴の中で再び進むには :forward コマンドを使います。
[factorial.hs:2:15-35] *Main> :history -1 : factorial (factorial.hs:2:30-34) -2 : factorial (factorial.hs:(1,1)-(2,35)) -3 : factorial (factorial.hs:2:19-35) -4 : factorial (factorial.hs:2:15-35) -5 : factorial (factorial.hs:(1,1)-(2,35)) <end of history> [factorial.hs:2:15-35] *Main> :back Logged breakpoint at factorial.hs:2:30-34 _result :: Integer n :: Integer 1 factorial 0 = 1 2 factorial n = n * factorial (n - 1) ^^^^^ 3 [-1: factorial.hs:2:30-34] *Main> :p n n = 20 [-1: factorial.hs:2:30-34] *Main> :forward Stopped at factorial.hs:2:15-35 _result :: Integer n :: Integer 1 factorial 0 = 1 2 factorial n = n * factorial (n - 1) ^^^^^^^^^^^^^^^^^^^^^ 3 [factorial.hs:2:15-35] *Main> :p n n = 19 [factorial.hs:2:15-35] *Main>
:back コマンドの直後でプロンプトに :history の左端の番号が出ています。
n が 20 であるような状態を再び確認することができました。
追跡 - :trace
ブレークポイントやステップ実行による停止以外で履歴を記録するのに
:trace コマンドを使うことができます。
ここでは factorial の終了条件のときまで止まらないように
ブレークポイントを設定しなおして :trace してみましょう。
:delete コマンドで番号を指定してブレークポイントを削除し、
終了条件の行にブレークポイントを設定してから :trace します。
[factorial.hs:2:15-35] *Main> :show breaks [0] Main factorial.hs:(1,1)-(2,35) [factorial.hs:2:15-35] *Main> :delete 0 [factorial.hs:2:15-35] *Main> :show breaks No active breakpoints. [factorial.hs:2:15-35] *Main> :b 1 Breakpoint 1 activated at factorial.hs:1:15 [factorial.hs:2:15-35] *Main> :show breaks [1] Main factorial.hs:1:15 [factorial.hs:2:15-35] *Main> :trace Stopped at factorial.hs:1:15 _result :: a = _ 1 factorial 0 = 1 ^ 2 factorial n = n * factorial (n - 1) [factorial.hs:1:15] *Main> :hist -1 : factorial (factorial.hs:2:30-34) -2 : factorial (factorial.hs:(1,1)-(2,35)) -3 : factorial (factorial.hs:2:19-35) -4 : factorial (factorial.hs:2:15-35) -5 : factorial (factorial.hs:2:30-34) -6 : factorial (factorial.hs:(1,1)-(2,35)) -7 : factorial (factorial.hs:2:19-35) -8 : factorial (factorial.hs:2:15-35) -9 : factorial (factorial.hs:2:30-34) -10 : factorial (factorial.hs:(1,1)-(2,35)) -11 : factorial (factorial.hs:2:19-35) -12 : factorial (factorial.hs:2:15-35) -13 : factorial (factorial.hs:2:30-34) -14 : factorial (factorial.hs:(1,1)-(2,35)) -15 : factorial (factorial.hs:2:19-35) -16 : factorial (factorial.hs:2:15-35) -17 : factorial (factorial.hs:2:30-34) -18 : factorial (factorial.hs:(1,1)-(2,35)) -19 : factorial (factorial.hs:2:19-35) -20 : factorial (factorial.hs:2:15-35) ... [factorial.hs:1:15] *Main> :back Logged breakpoint at factorial.hs:2:30-34 _result :: Integer n :: Integer 1 factorial 0 = 1 2 factorial n = n * factorial (n - 1) ^^^^^ 3 [-1: factorial.hs:2:30-34] *Main> :p n n = 1 [-1: factorial.hs:2:30-34] *Main>
履歴を記録しつつ終了条件まで到達することができました。
一つ前の履歴では n が 1 となっています。
評価が遅延している値
最後に評価が遅延している値を GHCi debugger がどのように扱っているか見てみましょう。
以下のようなクイックソートのプログラムを例にとります。
qsort.hs
qsort [] = [] qsort (a:as) = qsort left ++ [a] ++ qsort right where (left,right) = (filter (<=a) as, filter (>a) as) main = print (qsort [8, 4, 0, 3, 1, 23, 11, 18])
プログラムを読み込んで、
qsort関数にブレークポイントを設定し、
関数の中にステップします。
left を :print しても中身が表示されていません。
これは left の値の計算が遅延していてまだ計算されていないからです。
% ghci qsort.hs GHCi, version 7.0.4: http://www.haskell.org/ghc/ :? for help Loading package ghc-prim ... linking ... done. Loading package integer-gmp ... linking ... done. Loading package base ... linking ... done. [1 of 1] Compiling Main ( qsort.hs, interpreted ) Ok, modules loaded: Main. *Main> :break qsort Breakpoint 0 activated at qsort.hs:(1,1)-(3,56) *Main> :set stop :list *Main> main Stopped at qsort.hs:(1,1)-(3,56) _result :: [a] = _ vv 1 qsort [] = [] 2 qsort (a:as) = qsort left ++ [a] ++ qsort right 3 where (left,right) = (filter (<=a) as, filter (>a) as) ^^ 4 [qsort.hs:(1,1)-(3,56)] *Main> :step Stopped at qsort.hs:2:16-47 _result :: [a] = _ a :: a = _ left :: [a] = _ right :: [a] = _ 1 qsort [] = [] 2 qsort (a:as) = qsort left ++ [a] ++ qsort right ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 3 where (left,right) = (filter (<=a) as, filter (>a) as) [qsort.hs:2:16-47] *Main> :p left left = (_t1::[a]) [qsort.hs:2:16-47] *Main>
例えば seq 関数を使って、leftの先頭の構造だけ評価を強制すると、
以下のように表示することができます。
[qsort.hs:2:16-47] *Main> left `seq` () () [qsort.hs:2:16-47] *Main> :p left left = 4 : (_t2::[Integer]) [qsort.hs:2:16-47] *Main>
構造を辿って評価を強制したいときには :force コマンドを使います。
[qsort.hs:2:16-47] *Main> :force left left = [4,0,3,1] [qsort.hs:2:16-47] *Main>
いずれの方法も、評価順序が変化することによって停止しなくなるケースでは注意が必要です。
まとめ
- GHCi debugger を使うと
- ブレークポイントによる停止、ステップ実行ができる。
- 記録された履歴をバックステップで戻って参照できる。
- 追跡機能で履歴を実行に沿ってまとめて記録できる。
- 遅延した値を表示するときに評価を強制する必要がある場合、停止するかどうかを注意して使いましょう。