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 を使うと
    • ブレークポイントによる停止、ステップ実行ができる。
    • 記録された履歴をバックステップで戻って参照できる。
    • 追跡機能で履歴を実行に沿ってまとめて記録できる。
    • 遅延した値を表示するときに評価を強制する必要がある場合、停止するかどうかを注意して使いましょう。