irb に show_source があることをもっと知らしめたい

要は以下の記事の繰り返しなのだが。

k0kubun.hatenablog.com

Kaigi on Rails _2022_ new というイベントの LT で、メソッド定義を探ろうという話があった。

speakerdeck.com

Rails のソースをシュッと眺めに行くという、非常に尊い良い発表でした。

Object のことは Object に聞け、は Ruby の非常に面白いところなので、Method を取り出して source_location を尋ねるのは一度体験して感動して欲しいんだけど、実務だとタイプ数の少ないやり方も知っておくと更に便利に使えるのでご紹介。

Pry の $

https://github.com/pry/pry

irb よりも色々できて便利な REPL、というのでよく使われている gem だと思う。その後 irb が追いついて追い越していったので多分使いどころは減ってきているが、既存のコードベースには入っていることも多いだろう。

pry には show-source (alias の $ をよく使う) という機能があって、メソッドの定義に非常に簡単にアクセスできる。

https://github.com/pry/pry/blob/v0.14.1/lib/pry/commands/show_source.rb

使い方:

$ bundle exec pry
[1] pry(main)> require "set"
=> true
[2] pry(main)> $ Set#merge

From: /opt/rubies/3.1.2/lib/ruby/3.1.0/set.rb:603:
Owner: Set
Visibility: public
Signature: merge(enum)
Number of lines: 9

def merge(enum)
  if enum.instance_of?(self.class)
    @hash.update(enum.instance_variable_get(:@hash))
  else
    do_with_enum(enum) { |o| add(o) }
  end

  self
end

メソッド表示時に Syntax Highlight も入っていて、非常に見やすい。

pry には pry-doc という plugin もあって、これを使うと C 言語で実装されているメソッドも読みに行けるのが好きです。

[1] pry(main)> $ puts

From: io.c (C Method):
Owner: Kernel
Visibility: private
Signature: puts(*arg1)
Number of lines: 9

static VALUE
rb_f_puts(int argc, VALUE *argv, VALUE recv)
{
    VALUE r_stdout = rb_ractor_stdout();
    if (recv == r_stdout) {
        return rb_io_puts(argc, argv, recv);
    }
    return forward(r_stdout, rb_intern("puts"), argc, argv);
}

最近 こちらのエントリ でも紹介されていた。(あれオレ)

IRBshow_source

irb にも show_source コマンドがある。というか pry の同機能が欲しくて id:k0kubun さんが入れた。

https://github.com/ruby/irb/blob/v1.4.2/lib/irb/cmd/show_source.rb

irbshow_sourceIRB::Color という Syntax Highlight の仕組みが使われていて、読みやすくなっている。

なお、irb の制限として「Rubyとしてvalidな入力しかできない」というのがあり、pry と同じ書き心地ではない ($ の alias は無いし、引数も文字列として渡さなければいけない) が、それでも irbshow_source があるのは最高です。というか、pry の show-source を多用しまくっていたので、これが無いと僕は pry を卒業できなかった。

そして、エントリにもある通り .irbrc に細工をすると $ を再現できる。

# .irbrc
IRB::Context.prepend(Module.new{
  def evaluate(line, *, **)
    case line
    when /\A\$ /
      line.replace("show_source #{line.sub(/\A\$ /, '').strip.dump}\n")
    end
    super
  end
})

軽く説明すると

  • IRB::Context.prepend
    • 入力を eval する部分に介入したいので IRB::Context#evaluateprepend で上書きする
  • Module.new{}
    • prepend && super するために無名モジュールを作っている
  • when /\A\$ /
    • 入力が $ で始まっていたら
  • line.replace("show_source #{line.sub(/\A\$ /, '').strip.dump}\n")
    • show_source "..." に書き換えて
  • super
    • evaluate を実行する

はい。

これでタイプ数少なく実装を読みに行けるようになった。show_source が期待と違った、という人もこれなら満足するんじゃないでしょうか。

$ irb
irb(main):001:0> $ Set#merge

From: /opt/rubies/3.1.2/lib/ruby/3.1.0/set.rb:605

  def merge(enum)
    if enum.instance_of?(self.class)
      @hash.update(enum.instance_variable_get(:@hash))
    else
      do_with_enum(enum) { |o| add(o) }
    end

    self
  end

=> nil

irb 上の入力を加工して eval する手段もこれで分かったので、自前の面白機能を育てていくと良いと思う。

ここがもう一声

pry の show-source--super がメチャクチャ便利なので、欲しい。

どんな機能かというと、show-source でメソッドを読んでいるときに super が出てきたら --super を付けると親クラスのメソッドを表示できるヤツです。

こういう C < B < A というクラスがあるとして、

class A
  def foo
    puts "A"
  end
end

class B < A
  def foo
    puts "B"
    super
  end
end

class C < B
  def foo
    puts "C"
    super
  end
end

--super を付けて呼ぶとこうなる。

[1] pry(main)> $ C.new.foo

From: (pry):13:
Owner: C
Visibility: public
Signature: foo()
Number of lines: 4

def foo
  puts "C"
  super
end
[2] pry(main)> $ C.new.foo --super

From: (pry):7:
Owner: B
Visibility: public
Signature: foo()
Number of lines: 4

def foo
  puts "B"
  super
end

複数回 --super を渡すと super_level が上がる。

[3] pry(main)> $ C.new.foo --super --super

From: (pry):2:
Owner: A
Visibility: public
Signature: foo()
Number of lines: 3

def foo
  puts "A"
end

というのが欲しいんだけど、引数に渡したオブジェクトの ancestors を --super の数だけ遡ればできそうではあるな。 .irbrc でやってみるかー。