No day younger than today

RubyとかRailsとか蒙古タンメンとか

いい加減 pp '=' * 100 とか書くのが面倒なので gem にした

こんにちは、ふーが です。

この記事は、僕の勤務先である永和システムマネジメントが主催している ESM Advent Calendar 2023 の 1 日目の記事です。

pp '=' * 100 してますか?

僕はしています。それはもう毎日のようにしています。

debug gem とかを使った方がいいとも思うのですが、お仕事のアプリケーションでは諸般の事情により使えなかったりすることもあります。また、毎回ブレイクポイントで止まって欲しいわけではなく、いくつかのテストを流しながらその時々の変数の状態を見たい、というケースもあるでしょう。

そんな時は大体

pp '=' * 100
pp variable.class
pp variable.something
pp '=' * 100

などと書くことになります。

面倒、とにかく面倒

そんな感じでデバッグをしている僕ですが、1 日に何十、何百回とこういうのを書くことになります。とても面倒です。それはもう面倒です。

gem にした

そんなわけで、面倒ではなくなる gem を作りました。その名も ByeByePp です。名前の由来はそのまんまです。 github.com

まだ出来立てほやほやです。

どんな gem ?

使い方としては README に書いてあるまんまです。が、ここでもさらっと書いておきます。

今までは以下のようにデバッグしていました。

a = 'This is A.'
b = 10
c = %w[one two three]

pp '=' * 40
pp a.class
pp a.inspect

pp '=' * 40
pp b.class
pp b.inspect

.
.

(まぁ pp はそもそも inspect をしてくれるのだけど…)

ByeByePp を使うと以下のようになります。

require 'byebye_pp'

a = 'This is A.'
b = 10
c = %w[one two three]

bbpp a
bbpp b
bbpp c

bbpp メソッドに出力対象を渡すだけです。

すると、以下のような出力が得られます。

========================================
class: String
inspect: "This is A."
========================================
class: Integer
inspect: 10
========================================
class: Array
inspect: ["one", "two", "three"]

手軽に使えてべんりですね。

実装

実装は非常にシンプルで byebye_pp/lib/byebye_pp.rb の 1 ファイルにすべてまとまっています。特別なことは何もしていません。最後に Kernel.include しているので、pp や puts のようにどこででも使えます。

ただ、このメソッドは最後に puts していて戻り値が nil なので、テストをどうしようかと思っていたのですが、expect { ... }.to output(...).to_stdout という書き方で標準出力をテストできると知って勉強になりました。

今後の展望

ひとまず最低限めんどくさい状況を回避できるようにはなったのですが、もう少し実現したいことがあります。

bbpp の引数として渡されたものを文字列として出力したい

これは must で早急にやりたいことなのですが、たとえば先ほどの例をもう一度見てみます。

require 'byebye_pp'

a = 'This is A.'
b = 10
c = %w[one two three]

bbpp a
bbpp b
bbpp c

出力は以下です。

========================================
class: String
inspect: "This is A."
========================================
class: Integer
inspect: 10
========================================
class: Array
inspect: ["one", "two", "three"]

これくらい単純ならいいのですが、変数 a の出力がどれで、変数 b の出力がどれなのかがぱっと見でわからないんですよね。全てが順次実行ならいいのですが、並列処理でコードの記述順と出力順が入れ替わるケースだったり、真偽値をいくつも出力したい場合とかはけっこう不便になりそうです。

例えばですが、出力が以下のようになるとうれしいなと思います。

========================================
[ a ]
class: String
inspect: "This is A."
========================================
[ b ]
class: Integer
inspect: 10
========================================
[ c ]
class: Array
inspect: ["one", "two", "three"]

一応、手元では TracePoint を使って以下のようにすることで上記のような出力にできました。

module ByebyePp
  def bbpp(obj)
    text = <<~TEXT
      #{'=' * 40}
      [ #{$var} ]
      class: #{obj.class}
      inspect: #{obj.inspect}
    TEXT

    puts text
  end
end

Kernel.include ByebyePp

TracePoint.trace(:line) do |tp|
  @b = tp.binding
end

TracePoint.trace(:call) do |tp|
  next unless tp.method_id == :bbpp

  objects = tp.binding.local_variable_get(:obj)
  $var = @b.local_variables.detect { |v| @b.local_variable_get(v) == objects }
end

ただ、これだと bbpp メソッドの引数が変数である場合は問題ないんですが、引数でない場合は $var が空になってしまいます。例えば以下のようにメソッド呼び出しの結果を確認したいケースなどです。

require 'byebye_pp'

a = 'This is A.'

bbpp a.method(:shufle).source_location

以下のような出力を得られるのがベストです。

========================================
[ a.method(:shufle).source_location ]
class: ...
inspect: ...

こういったケースへの対応策は今のところわかっていない状況です…。どなたかアドバイスをもらえたらうれしいです。

出力内容を柔軟にしたい

これは願望レベルでまだ実装については何も考えていないのですが、今は class と inspect だけ出力しているのを柔軟にしたいというのがあります。例えば以下のように書けるとうれしいかなーと考えています。

require 'byebye_pp'

a = 'This is A.'

bbpp a, methods
========================================
a.methods: [:encoding, :force_encoding, :slice, ...]

なんか Object#send とかを使う形になりそうでおもしろそうです。が、まだドッグフーティングがあまりできていなくて本当にこれが使いやすいのかどうなのかわからないのでもう少し様子見かなぁ。

おわりに

日々のちょっとしたストレスを自分で解消できたのはちょっと自信になりました。もっとメタプロに詳しくなればやりたいことに対しての実現のためのアイディアの幅が広くなりそうだと思い、メタプログラミングRuby 第2版 を読み始めたりしています。

また日々のちょっとしたストレスがあれば gem なりメタプロなりを使って解決したい気持ちです。

おわり