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(
なぜでしょう。
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)
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
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)
require 'pry'
p Object.method_defined?(:pry)
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
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
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
の部分です。
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。
最後まで読んでいただきありがとうございます🙇♂️
参考