SEAN_BLOG

プログラミング etc.

binding.pry がなぜ動くのか?今更ながら調べてみた

TL;DR

  • binding.pry は、Kernel#bindingObject#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

こちらのメソッドは渡されたtargetbinding を返します。 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_stackoptions[: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#readPry#evalKernel#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 の戻り値のコンテキストで、文字列codeRuby のコードとして評価します。 ちなみに、多少処理がなされていますがcode の中身は、先ほどのPry::REPL#read の返り値です。

結局のところ、対象のbinding に対して、binding#eval を呼び出しているだけですね。

さて、ここまででreadeval までの処理が完了しましたね。

次は表示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
    ...

引数のobjectString 以外の時は、 キーワードsuper を呼び出し親クラスPPpp メソッドを呼び出しています。 それ以外のケースでは、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

最後まで読んでいただきありがとうございます🙇‍♂️

参考

*1:gem のインストールが必要。

*2:でつ: 社のSlack でご活躍のスヌーピーさん。

*3:実際にはBasicObject クラス(Ruby 1.9 以降)という、Object クラスの親にあたるクラスが存在します。

*4:コード量計測には'AlDanial/cloc' を使用。

*5:pry は様々なコマンドが利用出来たり、ハイライトが効いたりと単純なREPL プログラム以上のものです。