TL;DR
binding.pryは、Kernel#bindingとObject#pryに分けられる。Kernel#bindingはコンテキストの環境情報を束縛したBindingオブジェクトを返す。Object#pryはREPL プログラム。
そもそも、binding.pry とは?
Ruby を使っている方なら、一度はbinding.pry を利用したことがあるでしょう。
そう、debug するときにお世話になるあれです。
しかしながら、なぜあのような挙動になるのか知っている人は少ないと思います。
コード中にbinding.pry を記述し当該のコードを実行するだけで*1、
なぜかプロンプトが出現します。
9: def name => 10: binding.pry 11: puts @name 12: end [1] pry(#<Sample>)>
なぜでしょう。
wakaranai...
ということなので、今更ながら調べてみました。
そもそも、pry とは何なのか。
Pry is a runtime developer console and IRB alternative with powerful introspection capabilities. Pry aims to be more than an IRB replacement. It is an attempt to bring REPL driven programming to the Ruby language.
cf. https://github.com/pry/pry#introduction
要はruntime developer console だと。 そして、REPL driven programming をRuby に導入することを目指すものらしい。
でつ。oO(はて。REPL とは何だろう。。。難しそう。。。)*2
REPL (Read-Eval-Print Loop) とは、入力・評価・出力のループのこと。 主にインタプリタ言語において、ユーザーとインタプリタが対話的にコードを実行する。
cf. https://ja.wikipedia.org/wiki/REPL
とのことです。
今回はこちらのREPL の処理を中心に、pry を紐解いていこうと思います。
と、その前に今回はbinding.pry がなぜ動くのか?という事なので、
Kernel#Binding メソッドについても調査していきます。
Kernel#binding
まずは、binding.pry の前半分の、binding。
これは、gem pry に依存するものではなく、Ruby 組み込みライブラリKernel モジュールのメソッドです。
変数やメソッドなどの環境の情報を束縛した、Binding クラスのオブジェクトを返します。
def sample name = 'John' binding end eval('puts name', sample) #=> John
cf. Binding
Kernel#eval の第二引数に渡すと、そのコンテキストで第一引数の文字列をRuby プログラムとして評価してくれます。
つまり、binding.pry を実行したときに必要な情報が取得できるのは、この人のおかげという事ですね。
でつ。oO(すてき。。。)
Object#pry
さて、binding.pry の残りの部分pry の部分を深掘りしていきます。
肝心の pry メソッドですが、gem pry のなかで定義されています。
class Object ... def pry(object = nil, hash = {}) if object.nil? || Hash === object # rubocop:disable Style/CaseEquality Pry.start(self, object || {}) else Pry.start(object, hash) end end ...
Open class を用いてObject クラスに、pry メソッドを定義しています。
Object クラスは全てのクラスのスーパークラス*3なので、Object#pry を定義することで、
全てのオブジェクトでpry メソッドが利用できるようになります。
実際にModule#method_defined? を使って確かめてみると。
p Object.method_defined?(:pry) #=> false require 'pry' p Object.method_defined?(:pry) #=> true
require 'pry' の前では Object クラスに pry が定義されていなにのに対し、
require 'pry' の後では Object#pry が定義されていることが確認できます。
さて、ここからはbinding.pry がコード中で実際に呼ばれたとき、何が起こるのか見ていきます。
コード中でbinding.pry を呼ぶと、 Object#pry にデフォルト引数object = nil が渡されます。
そして、Object#pry の内部で、Pry.start(self, object || {}) が呼ばれます。
def self.start(target = nil, options = {}) ... options[:target] = Pry.binding_for(target || toplevel_binding) ... driver = options[:driver] || Pry::REPL # Enter the matrix driver.start(options) ... end
Pry.start は全体で30行程あります。
長くなるのでメインの処理の関係する箇所のみ抜粋しました。
Pry.binding_for が呼ばれています。target が存在すれば target を、
無ければトップレベルスコープの binding が引数として渡されます。
binding.pry のケースでは、self(=binding) がPry.binding_for の第一引数として渡されます。
def self.binding_for(target) return target if Binding === target # rubocop:disable Style/CaseEquality return TOPLEVEL_BINDING if Pry.main == target target.__binding__ end
こちらのメソッドは渡されたtarget のbinding を返します。
binding.pry のケースでは、self(=binding) が戻り値となります。
options[:target] = Pry.binding_for(target || toplevel_binding)
その結果、options[:target] にself(=binding) がアサインされます。
Pry.start では続いてdriver に Pry::REPL がアサインされ、Pry::REPL.start が呼ばれます。
options が引数として渡されるので、先ほどoptions[:target] としてアサインした束縛情報ももれなく渡されています。
def self.start(options) new(Pry.new(options)).start end ... def start prologue Pry::InputLock.for(:all).with_ownership { repl } ensure epilogue end
Pry::REPL のインスタンスが作成され、Pry::REPL#start が呼ばれます。
Pry のインスタンスが生成され引数として渡されています。
また、この際Pry のインスタンス変数@binding_stack にoptions[:target] の中身がアサインされています。
やっと、主要の処理にたどり着きました。Pry::REPL#repl です。
感の良い方はもうお気付きですね!(感が鈍くても気がつきますね。。。)
そう、Pry::REPL#repl は、先ほど説明したRead-Eval-Print Loop です!
def repl loop do case val = read when :control_c output.puts "" pry.reset_eval_string when :no_more_input output.puts "" if output.tty? break else output.puts "" if val.nil? && output.tty? return pry.exit_value unless pry.eval(val) end end end
コードを見てみると、何だか色々やってそうですが、
重要なのはPry::REPL#read 、Pry#eval、Kernel#loop です。
でつ。oO(あれ、print が無い。。。)
read
Pry::REPL#read > Pry::REPL#read_line > Pry::REPL#input_readline という順で見ていきます。
def input_readline(*args) Pry::InputLock.for(:all).interruptible_region do input.readline(*args) end end
input は@pry 経由でPry::Config からdelegate されています。
class Config ... def initialize merge!( input: MemoizedValue.new { lazy_readline }, ...
def lazy_readline require 'readline' ::Readline ... end
長くなりそうなので中間の処理を省きます。
上記の通りinput には::Readline がアサインされるので、
Readline.readline が呼ばれ、ユーザからの入力を取得します。cf. Readline
これがコード実行中にプロンプトが表示されて、入力待機状態になる現象の実態ですね。 無事ユーザからの入力部分は理解できました。
続いて、評価部分のeval を見ていきます。
eval
前出のPry::REPL#repl を見るとpry.eval(val) となっており、先ほど見たread の戻り値をPry#eval に渡しています。
ちなみに、ここで呼ばれているeval は、Pry クラスでoverride されており、Kernel#eval とは異なります。
Pry#eval > Pry#handle_line > Pry#evaluate_ruby と読み進めていきます。
def evaluate_ruby(code) inject_sticky_locals! exec_hook :before_eval, code, self result = current_binding.eval(code, Pry.eval_path, Pry.current_line) set_last_result(result, code) ensure update_input_history(code) exec_hook :after_eval, result, self end
こちらも途中の処理をだいぶ省きました。
Pry#evaluate_ruby の中で、current_binding.eval が呼ばれています。
Pry#current_binding は、binding_stack に格納されている現状のBinding オブジェクトを返します。
Pry#current_binding の戻り値のコンテキストで、文字列code をRuby のコードとして評価します。
ちなみに、多少処理がなされていますがcode の中身は、先ほどのPry::REPL#read の返り値です。
結局のところ、対象のbinding に対して、binding#eval を呼び出しているだけですね。
さて、ここまででread、eval までの処理が完了しましたね。
次は表示print の部分です。
実は先ほど見たPry#eval の中にprint の処理も含まれています。
Pry#eval > Pry#handle_line > Pry#show_result と掘り進めます。
def handle_line(line, options) ... Pry.critical_section do show_result(result) end ... end
def show_result(result) if last_result_is_exception? exception_handler.call(output, result, self) elsif should_print? print.call(output, result, self) end ... end
print の中身は、Pry::ColorPrinter.method(:default) の戻り値Method オブジェクトです。
そのMethod オブジェクトにcall メソッドを呼んでメソッドを実行しています。
require 'pp' ... class Pry class ColorPrinter < ::PP ... def self.default(_output, value, pry_instance) pry_instance.pager.open do |pager| pager.print pry_instance.config.output_prefix pp(value, pager, pry_instance.output.width - 1) end end ... end end
Pry::ColorPrinter.default の中で、pry_instance.pager.open によってfile を開きます。
ここで開くfile に関しては、設定によって変わります。
ブロックの中で、Pry::ColorPrinter.pp を呼び出します。
...
def self.pp(obj, output = $DEFAULT_OUTPUT, max_width = 79)
queue = ColorPrinter.new(output, max_width, "\n")
queue.guard_inspect_key { queue.pp(obj) }
queue.flush
output << "\n"
end
そして、ColorPrinter#pp を呼び出します。
...
def pp(object)
return super unless object.is_a?(String)
text(object.inspect)
rescue StandardError => exception
raise if exception.is_a?(Pry::Pager::StopPaging)
text(highlight_object_literal(inspect_object(object)))
end
...
引数のobject がString 以外の時は、
キーワードsuper を呼び出し親クラスPP のpp メソッドを呼び出しています。
それ以外のケースでは、ColorPrinter#text を呼び出します。
...
def text(str, max_width = str.length)
if str.include?("\e[")
super("#{str}\e[0m", max_width)
elsif str.start_with?('#<') || %w[= >].include?(str)
super(highlight_object_literal(str), max_width)
else
super(SyntaxHighlighter.highlight(str), max_width)
end
end
...
表示する文字列str の内容に応じて処理を変更しています。
基本的にはこちらもキーワードsuper により、親クラスのPP#text を呼び出しています。
この一連の処理によって、結果が出力されます。
loop
あとはこれを外側のループで回すだけです。
でつ。oO(loop は特に書くことが無い。。。)
ここまでの一連の流れがbinding.pry ないしは、pry メソッドの正体です。
pry というgem 全体で見ると処理も多く、コード数もそれなりにあります(20,000 行強*4)。
ただ、REPL に関する処理は、意外とシンプルなようでした。
所感
今まで何となく使っていたbinding.pry ですが、実際に調べてみて、
ブラックボックス化されていたことを明らかにすることができました。
# 今まで binding.pry == 便利な魔法の呪文 # 今 binding.pry == REPL プログラム
頭の中でbinding.pry の単語をアップデートすることができました*5。
最後まで読んでいただきありがとうございます🙇♂️
