GHCiデバッガ

GHCiは単純な命令的スタイルのデバッガを搭載していて、実行中の計算を停めて変数の値を確かめることができる。このデバッガはGHCiに統合されており、デフォルトで有効になっている。デバッグ機能を使うのにフラグは必要ない。一つ、重要な制限があって、ブレークポイントとステップ実行は解釈実行されているモジュールでしか使えない。コンパイル済みコードはデバッガからは見えない[5]

このデバッガは以下のものを提供する。

現在のところ、“スタックトレース”を得る手段は提供されていないが、追跡と履歴の機能が有用な次善策になっていて、これらで十分エラー発生時の状況を知ることができることがしばしばある。例えば、例外が投げられたときに自動的にブレークするようにすることが可能で、これは例外がコンパイル済みコードで投げられた場合にも有効である。(2.5.6. 例外をデバッグするを見よ)

ブレークポイントと変数内容の表示

全体を通した例としてクイックソートを使おう。コードは以下の通り。

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])

まず、このモジュールをGHCiにロードする。

Prelude> :l qsort.hs
[1 of 1] Compiling Main             ( qsort.hs, interpreted )
Ok, modules loaded: Main.
*Main>
      

ここで、qsortの二番目の等式の右辺にブレークポイントを設定する。

*Main> :break 2
Breakpoint 0 activated at qsort.hs:2:15-46
*Main>

:break 2というコマンドは、直近にロードされたモジュール(この場合qsort.hs)の2行目にブレークポイントを設定するものである。詳しく言うと、その行にある完全な部分式のうちもっとも左側にあるものが選ばれる。この場合(qsort left ++ [a] ++ qsort right)である。

さて、プログラムを実行してみる。

*Main> main
Stopped at qsort.hs:2:15-46
_result :: [a]
a :: a
left :: [a]
right :: [a]
[qsort.hs:2:15-46] *Main>

ブレークポイントのところで実行が中断した。プロンプトが変わって、現在ブレークポイントで停止していることと、その場所が[qsort.hs:2:15-46]であることを示している。場所をさらに明確にするには、:listコマンドが使える。

[qsort.hs:2:15-46] *Main> :list 
1  qsort [] = [] 
2  qsort (a:as) = qsort left ++ [a] ++ qsort right
3    where (left,right) = (filter (<=a) as, filter (>a) as)

:listコマンドは、現在のブレークポイントのまわりのコードを表示する。出力デバイスが対応しているなら、注目している部分式が太字で強調される。

ブレークポイントの置かれた式の各自由変数(aleftright)について、その束縛がGHCiに用意されている[6]。さらに、この式の結果についての束縛(_result)も用意される。これらの変数は、GHCi上で普通に定義する変数と同じである。つまり、プロンプトに打ち込む式の中で使ったり、:typeを使って型を問うたりできる。ただし、一つ重要な相違点があって、これらの変数は不完全な型しか持たないかもしれない。例えば、leftの値を表示しようとすると次のようになる。

[qsort.hs:2:15-46] *Main> left

<interactive>:1:0:
    Ambiguous type variable `a' in the constraint:
      `Show a' arising from a use of `print' at <interactive>:1:0-3
    Cannot resolve unknown runtime types: a
    Use :print or :force to determine these types

原因は、qsortが多相関数であって、GHCiが型情報を実行時に保持しないので、型変数に関係した自由変数の実行時の型を決定することができないことだ。これにより、プロンプトでleftを表示しようとすると、GHCiはShowのどのインスタンスを使うべきか判断できず、上のエラーを出力する。

幸いにも、デバッガには:printという汎用印字コマンドが搭載されている。このコマンドで、変数の実行時の値を調べ、その型を再構築することができる。これをleftに対して試してみると次のようになる。

[qsort.hs:2:15-46] *Main> :set -fprint-evld-with-show
[qsort.hs:2:15-46] *Main> :print left
left = (_t1::[a])

これでは大したことは分からない。何が起きたかというと、leftが未評価の計算(中断された計算、サンクともいう)に束縛されており、しかも:printは決して評価を強制しないのだ。これは、ブレークポイントで:printを使って値を調べるとき、望ましくない副作用を起こさずに済むようにするためだ。評価を強制しないので、プログラムが通常と異なる結果を与えることもないし、例外が発生することもないし、無限ループや別のブレークポイント(2.5.3. ブレークポイントのネスト)に遭遇することもない。:printは、サンクをforceする代わりに、各サンクをアンダースコアから始まる新しい変数(ここでは_t1)に束縛する。

-fprint-evld-with-showというフラグは、:printが可能な限り既存のShowインスタンスを再利用するようにするものである。これが起こるのは、対象の変数の中身が完全に評価済みの時だけである。

変数の評価状態を変えても構わない場合、:printの代わりに:forceを使うことができる。:forceコマンドは、サンクに遭遇する度に評価を強制するが、この点を除いて:printとまったく同じように振る舞う。

[qsort.hs:2:15-46] *Main> :force left
left = [4,0,3,1]

ここで、:forceleftの実行時の値を調べたので、この型が再構築された。この再構築の結果は次のようにして見ることができる。

[qsort.hs:2:15-46] *Main> :show bindings
_result :: [Integer]
a :: Integer
left :: [Integer]
right :: [Integer]
_t1 :: [Integer]

leftの型が分かっただけでなく、他の部分的な型もすべて解決されている。よって、例えばaの値を問い合わせることができる。

[qsort.hs:2:15-46] *Main> a
8

式全体の評価を:forceで強制するのでなく、個々のサンクを評価したい場合、Haskellのseqが便利かもしれない。例えば以下のように使う。

[qsort.hs:2:15-46] *Main> :print right
right = (_t1::[Integer])
[qsort.hs:2:15-46] *Main> seq _t1 ()
()
[qsort.hs:2:15-46] *Main> :print right
right = 23 : (_t2::[Integer])

ここでは、サンク_t1だけを評価し、これでリストの頭部が分かった。尾部は別のサンクで、_t2に束縛された。ここではseq関数は少々使いにくいので、:defを使ってもっと良いインタフェースをつけると良いかもしれない。(後は読者への練習問題としよう!)

最後に、現在の実行を再開することができる。

[qsort.hs:2:15-46] *Main> :continue
Stopped at qsort.hs:2:15-46
_result :: [a]
a :: a
left :: [a]
right :: [a]
[qsort.hs:2:15-46] *Main> 

実行は、前に停止した点から再開し、同じブレークポイントで再び停止した。

ブレークポイントを設定する

ブレークポイントを設定する方法はいくつかある。おそらくもっとも簡単な方法は最上位の関数の名前を使うことだろう。

   :break identifier

identifierは最上位の関数の名前(修飾名も使える)である。ただし、この関数の定義されているモジュールが、現在GHCiにロードされており、さらに解釈実行されていなければならない。ブレークポイントは関数の本体、関数が完全に適用されたがパターン照合が行われる前の点に設定される。

行番号(と列番号)でブレークポイントを設定することもできる。

   :break line
   :break line column
   :break module line
   :break module line column 

ブレークポイントを特定の行に設定する場合、GHCiは、その行で始まりその行で終わる部分式の中でもっとも左側にあるものを選ぶ。二つの完全な部分式が同じカラムから始まっているなら長い方が選ばれる。その行に完全な部分式が無い場合、その行で始まっている部分式の中でもっとも左側にあるものが選ばれる。それも失敗したら、その行を一部か全部覆う式の中でもっとも右側にあるものが選ばれる。

ブレークポイントを特定の行の特定のカラムに設定する場合、GHCiはその地点を含む式の中で最小のものを選ぶ。注意: GHCは、TAB文字を、現れた場所に関わらず幅1とみなす。言い換えれば、カラム数を数えるのではなく文字を数えている。これはある種のエディタの振る舞いと整合するが、そうでないエディタもある。最善なのはそもそもソースコード中でタブ文字を使わないことである。(4.8. 警告と正気度チェックのためのオプション中の-fwarn-tabsを見よ)

モジュールが省略された場合、直近にロードされたモジュールが使われる。

ブレークポイントを設定できない部分式もある。単一の変数は通常ブレークポイント位置とはみなされない。(ただし、その変数が関数定義かラムダかcaseの選択肢の右辺である場合は除く)。大まかに、ブレークポイントであるのは、全ての簡約基と、関数やラムダの本体、caseの選択肢、束縛文である。通常let式はブレークポイントでないが、その本体は常にブレークポイントである。そのletで束縛された変数の値を調べたいと思うのが普通だからである。

ブレークポイントを一覧・削除する

現在有効にされているブレークポイントの一覧は:show breaksで表示できる。

*Main> :show breaks
[0] Main qsort.hs:1:11-12
[1] Main qsort.hs:2:15-46

ブレークポイントを削除するには、:show breaksコマンドの出力にある数字を与えて:deleteコマンドを使えば良い。

*Main> :delete 0
*Main> :show breaks
[1] Main qsort.hs:2:15-46

全てのブレークポイントを一度に削除するには、:delete *とすれば良い。

ステップ実行

ステップ実行は、プログラムの実行を可視化する偉大な方法であり、バグの原因を同定する手段としても有用である。:stepコマンドを使うと、プログラム中の全てのブレークポイントが有効にされ、次のブレークポイントに達するまで実行される。:steplocalとすれば、現在のトップレベル関数の中にあるブレークポイントのみ有効にする。同様に、:stepmoduleとすると現在のモジュール中にあるブレークポイントのみ有効にする。例えば以下のようになる。

*Main> :step main
Stopped at qsort.hs:5:7-47
_result :: IO ()

:step exprコマンドは、exprのステップ実行を開始する。exprが省略された時は、現在のブレークポイントからステップ実行する。:stepoverも同様に動作する。

ステップ実行中には:listが特に便利で、自分がどこにいるか知ることができる。

[qsort.hs:5:7-47] *Main> :list
4  
5  main = print (qsort [8, 4, 0, 3, 1, 23, 11, 18])
6  
[qsort.hs:5:7-47] *Main>

実際、GHCiには、ブレークポイントに達したときに決まったコマンドを実行する方法があるので、自動的に:listするようにできる。

[qsort.hs:5:7-47] *Main> :set stop :list
[qsort.hs:5:7-47] *Main> :step
Stopped at qsort.hs:5:14-46
_result :: [Integer]
4  
5  main = print (qsort [8, 4, 0, 3, 1, 23, 11, 18])
6  
[qsort.hs:5:14-46] *Main>

ブレークポイントのネスト

GHCiがブレークポイントで停止したときに、プロンプトに入力された式が別のブレークポイントを起動した場合、この新しいブレークポイントが「現在の」ブレークポイントになり、古いものはスタックに保存される。このようにして、任意の数のブレークポイント文脈を作り上げることができる。例えば以下のように。

[qsort.hs:2:15-46] *Main> :st qsort [1,3]
Stopped at qsort.hs:(1,0)-(3,55)
_result :: [a]
... [qsort.hs:(1,0)-(3,55)] *Main>

前に設定した2行目のブレークポイントで停止したところで、:step qsort [1,3]として新しい評価を開始した。この新しい評価は一ステップの後に(qsortの定義で)停止した。ここで、プロンプトが変わって先頭に...が付き、現在のブレークポイントの他に保存されたブレークポイントがあることを示している。この文脈スタックを見るには、:show contextを使えば良い。

... [qsort.hs:(1,0)-(3,55)] *Main> :show context
--> main
  Stopped at qsort.hs:2:15-46
--> qsort [1,3]
  Stopped at qsort.hs:(1,0)-(3,55)
... [qsort.hs:(1,0)-(3,55)] *Main>

現在の評価を放棄するには、:abandonが使える。

... [qsort.hs:(1,0)-(3,55)] *Main> :abandon
[qsort.hs:2:15-46] *Main> :abandon
*Main>

_resultという変数

ブレークポイントで停止したときやステップ実行するとき、GHCiは_resultという変数を用意して、現在注目されている式の結果に束縛する。_resultの値はおそらくまだ存在しない(その評価を止めたのだから)が、その評価を強制することはできる。その型が既知でshowできるなら、_resultとプロンプトに入力するだけで表示できる。ただし、これを行うに当たって警告が一つある。_resultを評価すると、さらにブレークポイントを起動する可能性が高いということだ。特に、:stepでなく真のブレークポイントで停止していたなら、このブレークポイントが最初に起動する。このため、_resultを評価する時には、おそらく即座に:continueを発行する必要がある。別の方法として、:forceはブレークポイントを無視するので、これを使うこともできる。

追跡と履歴

プログラムをデバッグしている時にしばしば問われる問いの一つに、「どうやってここに来たんだ?」というものがある。伝統的な命令的デバッガは通常何らかのスタックトレースの機能を持っていて、それを使ってアクティブな関数呼び出しのスタック(字句的呼び出しスタックと呼ばれることもある)を確認することができる。このスタックによって、現在の地点に至るまでのコード上の道のりが分かる。残念なことに、これをHaskellで用意するのは難しい。正格な言語と違って、実行は深さ優先ではなく必要に応じて進むからである。GHCの実行エンジンにある「スタック」は字句的呼び出しスタックとは大きく異なる。理想的には、GHCが、この動的な呼び出しスタックに加えて、別に字句的呼び出しスタックを管理すれば良く、事実、我々のプロファイルシステム(第5章. プロファイルを取る)はまさにこれを行っており、他のHaskellデバッガにもこれをしているものがある。しかし、現在のところ、GHCiは字句的呼び出しスタックを管理しない。(克服せねばならない技術的困難がいくつかある)。代わりに、ブレークポイントから直前の評価ステップに戻る方法を用意している。これは要するにステップ実行を逆向きにするのと同じで、多くの場合「どうやってここに来たんだ?」という疑問を解決するのに十分な情報を与える。

追跡機能を使うには、式を:traceコマンドで評価する。例えば、qsortのベースケースにブレークポイントを設定する。

*Main> :list qsort
1  qsort [] = [] 
2  qsort (a:as) = qsort left ++ [a] ++ qsort right
3    where (left,right) = (filter (<=a) as, filter (>a) as)
4  
*Main> :b 1
Breakpoint 1 activated at qsort.hs:1:11-12
*Main> 

そして、小さなqsortを追跡付きで実行する。

*Main> :trace qsort [3,2,1]
Stopped at qsort.hs:1:11-12
_result :: [a]
[qsort.hs:1:11-12] *Main>

これで、評価ステップの履歴を確かめることができる。

[qsort.hs:1:11-12] *Main> :hist
-1  : qsort.hs:3:24-38
-2  : qsort.hs:3:23-55
-3  : qsort.hs:(1,0)-(3,55)
-4  : qsort.hs:2:15-24
-5  : qsort.hs:2:15-46
-6  : qsort.hs:3:24-38
-7  : qsort.hs:3:23-55
-8  : qsort.hs:(1,0)-(3,55)
-9  : qsort.hs:2:15-24
-10 : qsort.hs:2:15-46
-11 : qsort.hs:3:24-38
-12 : qsort.hs:3:23-55
-13 : qsort.hs:(1,0)-(3,55)
-14 : qsort.hs:2:15-24
-15 : qsort.hs:2:15-46
-16 : qsort.hs:(1,0)-(3,55)
<end of history>

履歴中の特定のステップを調べるには、:backを使う。

[qsort.hs:1:11-12] *Main> :back
Logged breakpoint at qsort.hs:3:24-38
_result :: [a]
as :: [a]
a :: a
[-1: qsort.hs:3:24-38] *Main> 

履歴中の各ステップで局所変数が保存されており、通常同様に調べることができる。また、プロンプトが変わって、-1と、履歴中の最初のステップを調べていることを示している。:forwardコマンドを使うと履歴中を前方に移動することができる。

:traceコマンドには式を与えても与えなくても良い。与えなかった場合、:stepの場合と同様、追跡は現在のブレークポイントから始まる。

履歴は:traceを使ったときしか記録されない。これは、全てのブレークポイントを履歴に記録することで性能が二倍以上悪化することが分かったからである。GHCiは最後の50ステップを履歴に記憶する。(将来、これを可変にするかもしれない)

例外をデバッグする

もう一つデバッグ中によく発生する疑問として、「この例外はどこから来たんだ?」というものがある。errorhead []によって発生する例外には文脈情報が付属していない。プログラム中のどのheadの呼び出しがエラーの原因になったかを調べるのは骨の折れる作業であり、通常Debug.Trace.traceを使ったり、プロファイル付きでコンパイルして+RTS -xc(5.3. 時間及び確保量のプロファイルを取るを見よ)を使ったりすることになる。

GHCiデバッガの提供する方法を使うと、このようなエラーを素早く、かつコードを再コンパイルすることなしに突き止めることが、うまくすればできるかもしれない。一つの方法は、ソースコード中で例外を投げた場所にブレークポイントを設定し、:trace:historyを使って文脈を把握することだろう。しかし、headはライブラリ中にあり、そこに直接ブレークポイントを設定する訳にはいかない。このために、GHCiには-fbreak-on-exceptionフラグが用意されていて、これを使うと、例外が投げられた時に評価器が停止するようにできる。-fbreak-on-errorも同様だが、これは例外が捕捉されなかった場合のみ停止する。例外によって停止したときは、GHCiはちょうどブレークポイントに当ったのと同じように振る舞うが、違いとして、ソースコード中の位置が表示されない。したがって、:traceと組み合わせて、例外が発生する直前のステップを記録するようにしないと、これらのコマンドはあまり有用でない。例えば次のように。

*Main> :set -fbreak-on-exception
*Main> :trace qsort ("abc" ++ undefined)
“Stopped at <exception thrown>
_exception :: e
[<exception thrown>] *Main> :hist
-1  : qsort.hs:3:24-38
-2  : qsort.hs:3:23-55
-3  : qsort.hs:(1,0)-(3,55)
-4  : qsort.hs:2:15-24
-5  : qsort.hs:2:15-46
-6  : qsort.hs:(1,0)-(3,55)
<end of history>
[<exception thrown>] *Main> :back
Logged breakpoint at qsort.hs:3:24-38
_result :: [a]
as :: [a]
a :: a
[-1: qsort.hs:3:24-38] *Main> :force as
*** Exception: Prelude.undefined
[-1: qsort.hs:3:24-38] *Main> :print as
as = 'b' : 'c' : (_t1::[Char])

投げられた例外そのものは、_exceptionという新しい変数に束縛される。

例外発生時にブレークする機能は、プログラムが無限ループしているとき、それが何をしているかを調べるのに特に便利である。単にCtrl-Cを叩いて、履歴を見て、何が起こっていたかを調べれば良い。

例: 関数を調べる

このデバッガを使って関数値を調べることは可能である。ブレークポイントで停止しており、スコープに関数があるとき、デバッガでその関数のソースコードを表示させることはできない。しかし、それを何らかの引数に適用して結果を観察することで、いくらかの情報を得ることはできる。

束縛が多相的な場合、この手順は少し複雑になる。例を使って示すことにしよう。単純のために、良く知られたmap関数を使う。

import Prelude hiding (map)

map :: (a->b) -> [a] -> [b]
map f [] = []
map f (x:xs) = f x : map f xs

mapにブレークポイントを設定し、呼ぶ。

*Main> :break 5
Breakpoint 0 activated at  map.hs:5:15-28
*Main> map Just [1..5]
Stopped at map.hs:(4,0)-(5,12)
_result :: [b]
x :: a
f :: a -> b
xs :: [a]

GHCiの伝えるところによって、fがスコープにあることが分かる。しかし、これの型はまだ完全には分かっていないので、これを何か引数に適用することはできない。それでも、最初の引数の型がxの型と同じで、結果の型が_resultの型と関係していることは分かる。(訳注: 原文は、Nevertheless, observe that the type of its first argument is the same as the type of x, and its result type is shared with _result.)

先に示した(2.5.1. ブレークポイントと変数内容の表示)ように、このデバッガはある程度賢く作ってあって、x_resultの型が解明されたときにはfの型を更新することができる。よって、この例ですべきことは、xを少しだけforceして、その型と、ひいてはfの引数部分の型を復元することである。

*Main> seq x ()
*Main> :print x
x = 1

これで、xの型が再構築され、それによってfの型も再構築されたことを確認できる。

*Main> :t x
x :: Integer
*Main> :t f
f :: Integer -> b

この時点で、好きなInteger型の引数にfを適用して、その結果を観察することができる。

*Main> let b = f 10
*Main> :t b
b :: b
*Main> b
<interactive>:1:0:
    Ambiguous type variable `b' in the constraint:
      `Show b' arising from a use of `print' at <interactive>:1:0
*Main> :p b
b = (_t2::a)
*Main> seq b ()
()
*Main> :t b
b :: a
*Main> :p b
b = Just 10
*Main> :t b
b :: Maybe Integer
*Main> :t f
f :: Integer -> Maybe Integer
*Main> f 20
Just 20
*Main> map f [1..5]
[Just 1, Just 2, Just 3, Just 4, Just 5]

最初のfの適用では、fの結果の型を復元するために、型の再構築をもう少し行わなければならなかった。しかし、それ以降は、fを通常の方法で自由に使うことができる。

制約

  • ブレークポイントで停止したとき、既に評価中の変数を評価しようとすると、二番目の評価はハングする。この理由は、その変数が評価中であることをGHCが知っているため、新しい評価では、先に進む前にその結果を待つが、最初の評価はブレークポイントで停止しているのだから、もちろん結果は得られない、ということにある。評価がハングしたときはCtrl-Cで中断してプロンプトに戻ることができる。

    これがもっともよく起こるのは、CAF(例えばmain)を評価していて、ブレークポイントで停止し、そのCAFの値を再びプロンプトで要求した場合である。

  • 暗黙パラメタ(7.8.3. 暗黙パラメタ)がブレークポイントで利用できるのは、明示的な型シグネチャがある場合だけである。

[5]

パッケージにはコンパイル済みコードしか入っていないので、パッケージをデバッグするにはそのソースを見つけてきて直接ロードするしかないことに注意。

[6]

元々、自由変数だけでなくスコープにあるあらゆる変数の束縛を用意していたが、これが性能にかなりの影響を与えることが分かった。そのため、現在は自由変数のみに制限している。