こんにちは、ふーが です。
この記事は、僕の勤務先である永和システムマネジメントが主催している 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 なりメタプロなりを使って解決したい気持ちです。
おわり