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_methodsbyebugコマンドの命令というわけではない。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命令は後で出てくる)