byebugのガイドをおおざっぱになぞってみる(2)
GUIDE.mdに沿って実際に動かしてみて、自分で自分に説明してみた記録。翻訳ではない。(というか英語的にどうかって言われると自信がない。)
Byebugのバージョンは9.0.6、Rubyのバージョンは2.4.0p0。
Introduction (続き)
Second Sample Session: Delving Deeper
ブレークポイント、コールスタック、リスタートを試そう。
お題はハノイの塔のパズルを解く、シンプルなRubyのプログラムだ。コマンドライン引数によって円盤の枚数を指定できるが、コマンドライン引数の取り扱いはプログラミング上の悩みの種でもある。
#
# ハノイの塔のパズルを解く
#
def hanoi(n, a, b, c)
hanoi(n - 1, a, c, b) if n - 1 > 0
puts "ディスクを#{a}から#{b}へ"
hanoi(n - 1, c, b, a) if n - 1 > 0
end
n_args = ARGV.length
raise('*** 引数を指定しないか、円盤の数を指定する') if n_args > 1
n = 3
if n_args > 0
begin
n = ARGV[0].to_i
rescue ValueError
raise("*** 整数で指定する。指定値: #{ARGV[0]}")
end
end
raise('*** 円盤の数は2から99まで') if n < 1 || n > 100
hanoi(n, :a, :b, :c)
前節で触れた通りメソッド定義のdef
も実行される。実行されるまではdef
で指定された名前のメソッドは定義されない。
まずはdef hanoi
が実行される前の時点で、どんなプライベートメソッドを呼び出せるか確かめてみよう。
$ byebug hanoi.rb
[1, 10] in /private/tmp/hanoi.rb
1: #
2: # ハノイの塔のパズルを解く
3: #
=> 4: def hanoi(n, a, b, c)
5: hanoi(n - 1, a, c, b) if n - 1 > 0
6:
7: puts "ディスクを#{a}から#{b}へ"
8:
9: hanoi(n - 1, c, b, a) if n - 1 > 0
10: end
(byebug) private_methods
[:include, :using, :public, :private, :define_method, :DelegateClass, :default_src_encoding, :sprintf, :format, :Integer, :Float, :String, :Array, :Hash, :fail, :iterator?, :__method__, :catch, :__dir__, :loop, :global_variables, :throw, :block_given?, :raise, :__callee__, :eval, :y, :Rational, :trace_var, :untrace_var, :Complex, :at_exit, :set_trace_func, :gem, :select, :caller, :caller_locations, :`, :test, :fork, :exit, :sleep, :respond_to_missing?, :Pathname, :gem_original_require, :load, :exec, :exit!, :system, :spawn, :abort, :syscall, :open, :printf, :print, :putc, :puts, :readline, :gets, :p, :readlines, :initialize_copy, :initialize_clone, :initialize_dup, :srand, :rand, :proc, :lambda, :trap, :require, :require_relative, :autoload, :autoload?, :binding, :local_variables, :warn, :method_missing, :singleton_method_added, :singleton_method_removed, :singleton_method_undefined, :initialize]
(byebug) private_methods.member?(:hanoi)
false
プライベートメソッドのリストの中に:hanoi
がないことが分かる。
ところでこのprivate_methods
はbyebug
コマンドの命令というわけではない。Rubyの機能だ。(実は前節のeval
もそうだ。)
byebug
コマンドは、命令として解せない未知の入力があったとき、それをRubyの式として評価する。ストップしている場所で好きにコードを書いてプログラムの状態を確認できる。
では、プログラムを進めるとどうなるだろうか。
(byebug) step
[5, 14] in /private/tmp/hanoi.rb
5: hanoi(n - 1, a, c, b) if n - 1 > 0
6:
7: puts "ディスクを#{a}から#{b}へ"
8:
9: hanoi(n - 1, c, b, a) if n - 1 > 0
10: end
11:
=> 12: n_args = ARGV.length
13:
14: raise('*** 引数を指定しないか、円盤の数を指定する') if n_args > 1
(byebug) private_methods.member?(:hanoi)
true
今度は:hanoi
が見付かった。
さて、コマンドライン引数のことだ。だが、実行時に引数を指定するのを忘れていたようだ。
(byebug) ARGV
[]
restart
命令を使ってしきり直そう。
restart
命令は、byebug
コマンドを再起動させる。指定した引数はコマンドライン引数として使われる。次のようにrestart 3
と入力するのは、byebug hanoi.rb 3
を実行したのと同じ状態になる。
(byebug) restart 3
Re exec'ing:
/Users/akira/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/byebug-9.0.6/bin/byebug /private/tmp/hanoi.rb 3
[1, 10] in /private/tmp/hanoi.rb
1: #
2: # ハノイの塔のパズルを解く
3: #
=> 4: def hanoi(n, a, b, c)
5: hanoi(n - 1, a, c, b) if n - 1 > 0
6:
7: puts "ディスクを#{a}から#{b}へ"
8:
9: hanoi(n - 1, c, b, a) if n - 1 > 0
10: end
これまでと同様、最初の実行行の前でストップしてプロンプトが表示されたらbreak 5
と入力する。
(byebug) break 5
Successfully created breakpoint with id 1
Byebugは、プログラムの実行を指定の場所でストップさせることができる。ストップさせる場所のことをブレークポイントと呼ぶ。
break
命令(略してb
)は、引数で指定した行をブレークポイントとして設定する。
ストップしているプログラムの実行を再開させるにはcontinue
命令(c
またはcont
)を使う。さっそく使ってみよう。
(byebug) continue
Stopped by breakpoint 1 at /private/tmp/hanoi.rb:5
[1, 10] in /private/tmp/hanoi.rb
1: #
2: # ハノイの塔のパズルを解く
3: #
4: def hanoi(n, a, b, c)
=> 5: hanoi(n - 1, a, c, b) if n - 1 > 0
6:
7: puts "ディスクを#{a}から#{b}へ"
8:
9: hanoi(n - 1, c, b, a) if n - 1 > 0
10: end
先ほど設定したブレークポイントで実行が止まる。
前節で説明したのと同じように、display
命令(disp
)を使えばブレークポイントでストップするたびに変数の値を表示させることができる。
(byebug) display n
1: n = 3
(byebug) display a
2: a = :a
(byebug) display b
3: b = :b
おっと、やりすぎた。b
の表示はここでは必要ない。表示を取り止めよう。
(byebug) undisplay 3
undisplay
命令(undisp
)は、引数で指定しの番号の表示設定を解除する。
指定する番号はdisplay
命令を実行した際に表示されたものを使う。設定された項目を確認したければdisplay
命令を引数なしで実行する。
(byebug) display
1: n = 3
2: a = :a
3: b = :b
実行再開。
(byebug) continue
Stopped by breakpoint 1 at /private/tmp/hanoi.rb:5
1: n = 2
2: a = :a
[1, 10] in /private/tmp/hanoi.rb
1: #
2: # ハノイの塔のパズルを解く
3: #
4: def hanoi(n, a, b, c)
=> 5: hanoi(n - 1, a, c, b) if n - 1 > 0
6:
7: puts "ディスクを#{a}から#{b}へ"
8:
9: hanoi(n - 1, c, b, a) if n - 1 > 0
10: end
もう一度。
(byebug) c
Stopped by breakpoint 1 at /private/tmp/hanoi.rb:5
1: n = 1
2: a = :a
[1, 10] in /private/tmp/hanoi.rb
1: #
2: # ハノイの塔のパズルを解く
3: #
4: def hanoi(n, a, b, c)
=> 5: hanoi(n - 1, a, c, b) if n - 1 > 0
6:
7: puts "ディスクを#{a}から#{b}へ"
8:
9: hanoi(n - 1, c, b, a) if n - 1 > 0
10: end
where
命令(backtrace
またはbt
)を使うと、プログラム実行の最初から、現在の場所に致るまでのメソッド呼び出しの階層(コールスタック)を調べられる。
(byebug) where
--> #0 Object.hanoi(n#Integer, a#Symbol, b#Symbol, c#Symbol) at /private/tmp/hanoi.rb:5
#1 Object.hanoi(n#Integer, a#Symbol, b#Symbol, c#Symbol) at /private/tmp/hanoi.rb:5
#2 Object.hanoi(n#Integer, a#Symbol, b#Symbol, c#Symbol) at /private/tmp/hanoi.rb:5
#3 <top (required)> at /private/tmp/hanoi.rb:28
(byebug)
where
命令が表示しているのは次の通り。-->
はByebugが今いる階層(フレーム)であることを示すマーク、#0
はフレーム番号、呼び出されているメソッド、ファイル名と行番号。#1
〜#3
も同様。
表示したコールスタックを読むと、最初にあったメソッド呼び出しは28行目(フレーム3)のhanoi
メソッドで、続いて5行目(フレーム2)、hanoi
メソッド自身がhanaoi
メソッドの呼び出したことが分かる。同様の呼び出しがさらにもう1回(フレーム1)。呼びされたのが現在の状況(フレーム0)だ。
同じ行番号が並んでややこしいが、ちょうどn
の値が1
なので、一行分だけ処理を進めてみよう。n - 1 > 0
の条件から外れるので7行目に進むはずだ。
(byebug) s
1: n = 1
2: a = :a
[2, 11] in /private/tmp/hanoi.rb
2: # ハノイの塔のパズルを解く
3: #
4: def hanoi(n, a, b, c)
5: hanoi(n - 1, a, c, b) if n - 1 > 0
6:
=> 7: puts "ディスクを#{a}から#{b}へ"
8:
9: hanoi(n - 1, c, b, a) if n - 1 > 0
10: end
11:
where
命令でコールスタックを表示させると、現在呼び出されているのがhanoi
メソッドで、その7行目の前でストップしていることが分かる。
(byebug) bt
--> #0 Object.hanoi(n#Integer, a#Symbol, b#Symbol, c#Symbol) at /private/tmp/hanoi.rb:7
#1 Object.hanoi(n#Integer, a#Symbol, b#Symbol, c#Symbol) at /private/tmp/hanoi.rb:5
#2 Object.hanoi(n#Integer, a#Symbol, b#Symbol, c#Symbol) at /private/tmp/hanoi.rb:5
#3 <top (required)> at /private/tmp/hanoi.rb:28
なお、ファイル名の表示がフルパスで長すぎるようならset nofullpath
としておくとよい。するとディレクトリ名の一部が省略されて表示される。
では次に、コールスタックを移動してみよう。
変数の表示はここでは必要ないのですべてまとめて解除しておく。undisplay
命令を引数なしで実行する。
(byebug) undisplay
Clear all expressions? (y/n) y
今いるフレーム0はhanoi
メソッドの中だから、変数n_args
にはアクセスできない。確かめてみよう。
(byebug) n_args
*** NameError Exception: undefined local variable or method `n_args' for main:Object
nil
大本の呼び出し元であるフレーム0にByebugの視点を移動する。frame
命令(f
)を使って次のようにフレーム番号を指定すると、その階層に移動できる。
(byebug) frame 3
[19, 28] in /private/tmp/hanoi.rb
19: begin
20: n = ARGV[0].to_i
21: rescue ValueError
22: raise("*** 整数を指定する。指定値: #{ARGV[0]}")
23: end
24: end
25:
26: raise('*** 円盤の数は2から99まで') if n < 1 || n > 100
27:
=> 28: hanoi(n, :a, :b, :c)
ここなら変数n_args
が見える。
(byebug) n_args
1
変数n
も見えるが、フレーム0で見えていたのとは別のn
だ。
(byebug) eval n
3
階層を下ってみる。down
命令を使うと、引数でした数だけ階層を深いほうへ移動できる。
(byebug) down 2
[1, 10] in /private/tmp/hanoi.rb
1: #
2: # ハノイの塔のパズルを解く
3: #
4: def hanoi(n, a, b, c)
=> 5: hanoi(n - 1, a, c, b) if n - 1 > 0
6:
7: puts "ディスクを#{a}から#{b}へ"
8:
9: hanoi(n - 1, c, b, a) if n - 1 > 0
10: end
何度も出てきた5行目だが、Byebugが今いるのはフレーム1だ。
(byebug) bt
#0 Object.hanoi(n#Integer, a#Symbol, b#Symbol, c#Symbol) at /private/tmp/hanoi.rb:7
--> #1 Object.hanoi(n#Integer, a#Symbol, b#Symbol, c#Symbol) at /private/tmp/hanoi.rb:5
#2 Object.hanoi(n#Integer, a#Symbol, b#Symbol, c#Symbol) at /private/tmp/hanoi.rb:5
#3 <top (required)> at /private/tmp/hanoi.rb:28
したがってこのフレームで見えるn
の値も変化する。
(byebug) eval n
2
ところで変数n_args
の値を見るのにはn_args
と入力したが、変数n
の値を見るのにはeval n
と入力している。これはn
とだけ入力するとnext
命令の省略型とみなされてしまうためである。(next
命令は後で出てくる)