遅延関数型言語でデバッグがどのように行われるか知りたいです。
ブレークポイント、print ステートメント、および従来の手法を使用できますか? これも良い考えですか?
モナドを除いて、純粋な関数型プログラミングでは副作用が許されないというのが私の理解です。
実行順序も保証されません。
テストしたいコードのセクションごとにモナドをプログラムする必要がありますか? この分野でより経験豊富な人から、この質問への洞察が欲しいです。
5 に答える
遅延評価された関数型プログラムでブレークポイントを使用することを妨げるものは何もありません。熱心な評価との違いは、プログラムがブレークポイントでいつ停止するか、およびトレースがどのように見えるかです。ブレークポイントが設定されている式が実際に削減されると、プログラムは停止します (明らかに)。
慣れ親しんだスタック トレースの代わりに、ブレークポイントを持つ式の削減につながる削減を取得します。
ちょっとばかげた例。このHaskellプログラムがあります。
add_two x = 2 + x
times_two x = 2 * x
foo = times_two (add_two 42)
そして、最初の行 ( ) にブレークポイントを置いてadd_two
から、 を評価しfoo
ます。プログラムがブレークポイントで停止すると、熱心な言語では次のようなトレースが期待されます
add_two
foo
まだ評価されtimes_two
ていませんが、GHCi デバッガでは
-1 : foo (debug.hs:5:17-26)
-2 : times_two (debug.hs:3:14-18)
-3 : times_two (debug.hs:3:0-18)
-4 : foo (debug.hs:5:6-27)
<end of history>
これは、ブレークポイントを設定した式のリダクションにつながったリダクションのリストです。明示的に呼び出していなくても、times_two
「呼び出された」ように見えることに注意してください。foo
このことから2 * x
、 (-2) の評価が行から (-1)times_two
の評価を強制したことがわかります。そこから、命令型デバッガーのようにステップを実行できます (次のリダクションを実行します)。(add_two 42)
foo
熱心な言語でのデバッグとのもう 1 つの違いは、変数がまだサンクとして評価されていない可能性があることです。たとえば、上記の trace と inspect のステップ -2 では、x
まだ評価されていないサンク (GHCi では括弧で示されます) であることがわかります。
より詳細な情報と例 (トレースをステップ実行する方法、値を検査する方法など) については、GHC マニュアルの GHCi Debugger セクションを参照してください。私は VIM と端末のユーザーなので、まだ使用していないLeksah IDEもありますが、マニュアルによると、GHCi デバッガーへのグラフィカルなフロントエンドがあります。
また、印刷ステートメントを要求しました。純粋な関数だけでは、print ステートメントが IO モナド内にある必要があるため、これはそれほど簡単には可能ではありません。だから、あなたは純粋な関数を持っています
foo :: Int -> Int
trace ステートメントを追加したい場合、print は IO モナドでアクションを返すため、その trace ステートメントを入れたい関数のシグネチャと、それを呼び出す関数のシグネチャを調整する必要があります。 ...
これは良い考えではありません。したがって、トレースステートメントを達成するには、純粋性を破る何らかの方法が必要です。Haskell では、これはunsafePerformIO
. Debug.Trace
すでに関数を持っているモジュールがあります
trace :: String -> a -> a
文字列を出力し、2 番目のパラメーターを返します。純粋な関数として書くことは不可能です (実際に文字列を出力するつもりなら)。unsafePerformIO
ボンネットの下で使用します。それを純粋な関数に入れて、トレースプリントを出力できます。
テストしたいコードのセクションごとにモナドをプログラムする必要がありますか?
私は逆に、できるだけ多くの関数を純粋にすることをお勧めします (ここでは、印刷用の IO モナドを意味していると仮定しています。モナドは必ずしも純粋ではありません)。遅延評価を使用すると、IO コードを処理コードから非常にきれいに分離できます。
命令型デバッグ手法が適切かどうかは、(いつものように) 状況によって異なります。QuickCheck/SmallCheck を使用したテストは、命令型言語での単体テストよりもはるかに有用であることがわかっているため、デバッグをできるだけ回避するために、最初にその方法を使用します。実際、QuickCheck のプロパティは、適切で簡潔な関数仕様を作成します (命令型言語の多くのテスト コードは、私には別のコードの塊のように見えます)。
多くのデバッグを回避する 1 つの方法は、関数を多くの小さなサブ関数に分解し、できるだけ多くのサブ関数をテストすることです。これは、命令型プログラミングの場合は少し珍しいかもしれませんが、使用している言語に関係なく、良い習慣です。
また、!= テストをデバッグし、どこかで問題が発生した場合は、ブレークポイントとトレースが役立つ場合があります。
この話題は短いスペースで扱えるとは思えません。次のリンクから入手できる論文をお読みください。
Haskell でひどく複雑なことを掘り下げたことはありませんが、副作用が実質的になくなったという事実により、デバッグの必要性はほとんどなくなりました。純粋な関数は、デバッガーなしでテストおよび検証するのが非常に簡単です。
一方で、モナド内で何かをデバッグする必要があったことを数回経験しました。
少なくとも小規模なプログラムやシステムの場合、デバッグは窓の外に出ます。強力な型付けと静的な型チェックにより、手続き型プログラミングで見つかった従来のバグがさらに解消されます。バグがあったとしても、そのほとんどは論理的なバグ (間違った関数、数学的なエラーなどと呼ばれます) であり、インタラクティブにテストするのは非常に簡単です。
Clojureの経験から(これは怠惰で機能的であり、純粋性を奨励しますが強制しません):
他の言語と同じようにブレークポイントを設定できます。ただし、遅延評価のため、これらはすぐに呼び出されない可能性がありますが、遅延構造が強制されるとすぐにヒットします。
副作用を許容する遅延関数型言語 (Clojure を含む) では、printlns やその他のデバッグ ログを比較的簡単に挿入できます。私は個人的にこれらが非常に便利だと思います。怠惰のためにこれらがいつ呼び出されるかについて注意する必要がありますが、出力がまったく表示されない場合は、怠惰のためにコードが評価されていないことを示唆している可能性があります.....
上記のすべてを述べたので、デバッガーに頼る必要は今のところありません。多くの場合、いくつかの簡単なテスト (おそらく REPL で) を実行するだけで、機能するコードが正しく機能していることを確認できます。これらのテストが失敗した場合、通常、何が問題なのかは明らかです。
怠惰の問題をデバッグするための独自のツールを宣伝させてください。すでに 2 日間のデバッグに費やした怠惰に関連するメモリ リークを 1 時間で解決するのに役立ちました。
http://www.haskell.org/pipermail/haskell-cafe/2012-January/098847.html