LoginSignup
41
37

More than 3 years have passed since last update.

もう puts/p をデバッグに使わない! デバッグライブラリ IceCream の Ruby 版

Last updated at Posted at 2021-01-29

先日、Python 向けのデバッグライブラリ IceCream話題になっていました。

Python 以外にもいくつかの言語版があるようですが、残念ながら Ruby 版が無い。というわけで作ってしまいました。

Ricecream (icecream-ruby)

はじめに

まず、このライブラリはいわゆる「print デバッグ」1の代替となるものです。

そもそも本格的にデバッグを行う場合、print デバッグではなく Byebug 等のデバッガを用いるべきです。

参考:printデバッグにさようなら!Ruby初心者のためのByebugチュートリアル

とはいえ print デバッグが非常に手軽なものであることは間違いなく、ちょっとしたデバッグにはいちいちデバッガを使うより print デバッグで済ませてしまうという方も多いでしょう。

しかし print デバッグは手軽な分そのままでは困ることも少なくありません。 print デバッグの手軽さはそのままに、もう少し便利にしてくれるライブラリが IceCream (そしてこの Ricecream )だと思います。

概要

print デバッグの困るところ

例えば、変数 foo の値を調べたいとします。print デバッグの場合、以下のようなコードを追加することになります。

p foo

この場合、123 のような形で foo の値が表示されます。

print デバッグをする箇所が 1 箇所であればこれで困りませんが、複数の変数をさまざまな場所で print デバッグしようとすると、先のコードでは出力されたのがどの変数なのか判りません。また、もともとさまざまな標準出力を行うプログラムの場合、print デバッグの出力値と本来の出力値が混ざってしまい見づらくなります。

以下のように書けば、そうした問題点を解決できます。

puts "[debug] foo: #{foo}" # => [debug] foo: 123

ですが、こんなのを入力するのは面倒です。2 これではせっかくの print デバッグの手軽さを台無しにしてしまいます。

Ricecream の場合

Ricecream を使えば、前者のような手軽な

ic foo

というコード 3 で、

ic| foo: 123

と表示することができるようになります。

変数等の表示

上の例では単に変数の値をそのまま表示させただけですが、ic は任意の式を取ることができます。また、複数の引数も受け取れますし、引数が複数行にまたがっていても問題ありません。

require "ricecream"
ic foo * 2,
   Integer.sqrt(foo)
# => ic| foo * 2: 246, Integer.sqrt(foo): 11

また、 ic は引数をそのまま戻り値にするので、既存のコードに簡単に追加できます。例えば以下のコードにおいて foo.bar の値を確認したいとしましょう。

return "result = #{foo.bar}"

puts を使う場合、

puts foo.bar
return "result = #{foo.bar}"

としてしまうと foo.bar が副作用を伴うようなメソッドだった場合、2 回呼び出されてしまうことで予期せぬ動作になる可能性があります。ですので

foobar = foo.bar
puts foobar
return "result = #{foobar}"

のように書くことになるでしょう。書くのも面倒ですし、デバッグ後の修正作業も面倒です。

一方、ic を使えば

return "result = #{ic foo.bar}"

と簡単に書けます。

…とはいえ ppp も同様に引数を戻り値にするので、この点に関してはあまり Ricecream の優位性はありません。

実行の流れを調べる

以下のようなメソッドが、foo に到達しなかったとします。

def mymethod
  return if cond1?
  return if cond2?
  return if cond3?
  foo
end

このとき print デバッグでは以下のように様々な出力値の p をあちこちに追加することで、どこで return したのかを調べることになるでしょう。例えば下の例で「1」「2」が出力されたのであれば、return if cond2? のところで return したことが分かります。…しかし、出力値を一つ一つ変えて入力する手間がかかりますし、どこで出力された値なのかが直感的には判りません。

def mymethod
  p 1
  return if cond1?
  p 2
  return if cond2?
  p 3
  return if cond3?
  p 4
  foo
end

一方、以下のように引数無しで ic を使えば

def mymethod
  ic
  return if cond1?
  ic
  return if cond2?
  ic
  return if cond3?
  ic
  foo
end
output
ic| script.rb:2 in mymethod at 23:59:52.822
ic| script.rb:4 in mymethod at 23:59:52.822

のように、どのスクリプトの何行目が実行されたのかが判りやすく表示されます。

限定されたスコープで ic を使う

Ricecream は Kernel を拡張してしまうので、使うのが不安だという方もいるかもしれません。

そんな方のために、Refinements 版もあります。

require "ricecream/refine"

using Ricecream
ic

また、using Ricecream::P すれば p をオーバーライドできます。

require "ricecream/refine"

using Ricecream::P
foo = 123
p foo # => ic| foo: 123

ic の表示を消す

print デバッグが終わった際に、うっかり追加した p を消し忘れてしまうことってありませんか?特に普段は実行されない部分に追加したところとか。

もちろん Ricecream を使った場合も ic を消し忘れる可能性はあります。ですが Ricecream.disable を実行しておけば、万が一消し忘れたとしても出力がされなくなるので安心です。

require "ricecream"
ic # => 出力される
Ricecream.disable
ic # => 出力されない
Ricecream.enable
ic # => 出力される

その他いろいろ

prefix を変更

prefix (デフォルトでは ic| )を変更できます。

Ricecream.prefix = "ricecream| "

メソッド Ricecream.prefix を定義することで、より柔軟な変更ができます。

def Ricecream.prefix(location)
  "ic #{Time.now}| "
end

出力先の変更

デフォルトでは出力先は STDERR ですが、これも変更できます。

  Ricecream.output = STDOUT
  def Ricecream.output(str)
    @log ||= Logger.new("logfile.log")
    @log.debug(str)
  end

出力方法の変更

デフォルトでは Object#inspect (つまり p 相当)を用いて値を出力しますが、Ricecream.arg_to_s を定義することで変更できます。

  def Ricecream.arg_to_s(arg)
    PP.pp(arg, +"", 60)
  end

呼び出し元情報を表示

Ricecream.include_context = true すると、ic を呼び出した場所の情報を付けて表示します。

  ic foo # => ic| foo: 123
  Ricecream.include_context = true
  ic foo # => ic| script.rb:10 in <main>- foo: 123

色付け(実験的機能)

Ricecream.colorize = true すると、色付きで表示します。ただし irb 1.2 以上が必要です。

対応できない場合

以下のような場合、ic は適切な表示ができません。

同じ行に、引数の数が同じ ic

ic foo; ic bar
# => ic| foo: 123
# => ic| foo: 456
#         ↑ bar なのに…

のように同じ行に引数の数が同じ ic があると、正しい表示ができません。

引数の数が異なれば問題ないので、無理やり引数を増やしてしまえば大丈夫です。

ic foo; ic bar, 1
# => ic| foo: 123
# => ic| bar: 456, 1

eval 中で ic

eval 中の ic には対応できません。以下のように、とりあえず引数を <arg1> のように表示します。

eval("ic foo") # => ic| (eval):1 in <main>- <arg1>: 123

引数展開

ic に引数を展開して与えると対応できません。しかし、そもそもそんな行為に何の意味があるのでしょうか?

ic *[1, 2, 3] # => ic| script.rb:4 in <main>- <arg1>: 1, <arg2>: 2, <arg3>: 3

以上のように対応できない場合はありますが、レアケースだと思うので大抵の状況には対応できるはずです。

まとめ

デバッガの使い方を覚えようと思いつつ ついつい面倒で print デバッグしてしまう方は、この Ricecream を使ってみてはいかがでしょうか?


  1. 実際には Ruby の場合 print ではなく putsp を使うでしょうが、そうした標準の出力メソッドを使ってデバッグを行うことの総称をここでは「print デバッグ」と呼ぶことにします。 

  2. 例えば VSCode であれば、スニペットを利用するなどして簡単化することもできるとは思いますが。 

  3. 省略しましたが、 require "ricecream" している前提です。 

41
37
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
37