プログラミング漫遊記

思ったことや、勉強したことをつらつらと。

困ったときはターミナルへ出力だ!プリントデバッグ入門

こんにちはフィヨルドブートキャンプで学習中のはるぐちです。

この記事はフィヨルドブートキャンプ Part 1 Advent Calendar 2021の14日目の記事になります。 前回はmasuyama13さんの「コードより先にコミットメッセージを書く」でした。

techblog.tebiki.co.jp

また、フィヨルドブートキャンプ Part 2 Advent Calendar 2021もあります。

対象読者

  • プログラミングを始めたばかりのあなた
  • フィヨルドブートキャンプ生
  • あの頃の自分

プログラミング経験がある人からすると「こんなん知ってるわ!!」となること間違いなしですが、過去の自分が知りたかったことをまとめた「超初心者向け」の内容ですので温かい眼差しで、目を細めて読んでいただけたら幸いです。

はじめに

「あれ?なんで動かないんだろ?」

プログラムを実行しようとすると必ずといっていいほど遭遇する(プログラムが思い通りに動かない)バグ。

僕もプログラミングを学習し始めた頃は、動かないプログラムに四苦八苦しました。(今でも)

英語で現れる読めないエラー。1文字のタイポのせいで空虚に消え去る1時間。変数の中身がnilだった時の絶望。「あぁ、無理だ。もう質問しようかなー」と分からないところを整理し始めた時に発見する解決方法。「絶対自分は間違ってないし」と強気に何度も実行するも「絶対的に自分が間違っている」現状。

いやですよね。プログラムが動かないのって。かっこよく1発で思い通りにプログラムを実行したいですよね。

安心してください!!1発でプログラムを動かそうなんてまぁ無理です!!

僕は2年間プログラミングを学習して(当初よりは)それなりにプログラムが書けるようになってきたとは思うんですが、プログラムは一向に動きません。ほとんど動きません。マジで全然動きません。たまに動きます。動くと可愛いです。

ということで、エラーを1回も吐かずに1発で実行できるプログラムを書くというのは土台無理な話なんですが、「プログラムが動かなくなった時に素早く原因を特定し、問題を解決できるようになる。」なら可能な気がします。そして、僕も、最初と比べたら問題解決までの時間が早くなった気がします。

この記事の方向性

プログラムが想定通り動かない時、原因を突き止め改善する作業ををデバッグといいます。デバッグをする手順としては以下のような手順が一般的です。

  1. プログラムが動かない、憤る
  2. エラーが出てないか確認する
  3. エラー文を見て怪しそうなところを調べる
  4. 怪しそうなところの変数やメソッドの戻り値が期待通りかどうか調べる
  5. 動く、喜ぶ

このようにデバッグをするときは、まずはエラー文を読め!ということになるんですが、この記事ではエラー文という謎の呪文をすっ飛ばして、とにかくターミナルに必要な情報を表示させるということに主眼を起きたいと思います。また、デバッグをするときはdebuggerなどの便利なツールもあるのですが、まずは最初に覚えたい「プリントデバッグ」という方法についてまとめていけたらなあと思います。

この方法を取得すると、エラーが出たときのみならず、コードを読むときに「この変数の中身なんだろう?」「このメソッドの戻り値点だっけ?」となった時も役立つと思います。

前置きが長くなりましたが、よろしくお願います。

ターミナルに出力して確かめる

メソッドを実行したときの戻り値や、変数の中身を知りたい時は「ターミナル(正確には標準出力)に出力するメソッド」を用います。次のコードを見てください。

# debug.rbというファイルに保存
numbers = [1, 2, 3, 4, 5]
even_numbers = numbers.select{ |num| num.even? }

ローカル変数even_numbersの中身に何が入っているのか知りたくなったとしましょう。(例が単純すぎて予測できますが、複雑なメソッドチェーンなどになっているときは分かりにくいですよね?ね?) この場合、pメソッドを使うとローカル変数even_numbersをターミナルに出力すことができます。

numbers = [1, 2, 3, 4, 5]

even_numbers = numbers.select{ |num| num.even? }
p even_numbers #デバッグのために追加

実行して確かめましょう

$ ruby debug.rb
[2, 4]

上の実行結果からeven_numbersは2と4というIntegerクラスのオブジェクトが入った配列であることがわかります。 ターミナル(標準出力)に出力するメソッドは他にもputsprintメソッドがありますが、それぞれ以下のような違いがあります。

p even_numbers
puts even_numbers
print even_numbers
$ ruby debug.rb
[2, 4]  # pメソッド(最後に改行されている)
2  # putsメソッド↓(配列の各要素が改行されている)
4
[2, 4] $ #printメソッド(改行されずにプロンプトが同一行にある)
メソッド 簡単な違い
p 配列の各要素を改行せずに表示し,全て出力した後で最後に改行が入る。データ構造(ここでは配列)がわかるので開発者にとって便利な情報が表示されている
puts 配列の各要素を改行して表示する。また、全て出力した後にも改行が入る。
print 1行で出力するため改行はない。上記では入力を受け付けるシェルのプロンプト$が同じ行に表示されているとこがわかりる。

色々違いがあることはわかったけど、どう使い分けるの?という疑問が湧いてくるかもしれません。 結論からいうとpメソッドは開発者にとって有益な情報を返してくれるので、基本的にはpメソッドを用いていれば大丈夫です。

pメソッドを使うとき注意点(結合度の違いに注意)

先ほどのコードを少し書き方を変えてpメソッドを使って式の評価結果を出力します。

numbers = [1, 2, 3, 4, 5]

# 例1
p numbers.select do |num|
  num.even?
end
=> #<Enumerator: [1, 2, 3, 4, 5]:select>

# 例2
p numbers.select do |num| num.even? end # 1行で書いてみたが、、、
=> #<Enumerator: [1, 2, 3, 4, 5]:select>

# 例3
p numbers.select{ |num| num.even? }
=> [2, 4]

do ~ endで記述(例1、2)したときと、{ ~ }で記述(例3)したときで出力結果が変わっています。どちらかというと{}で囲んだ時に出力されている情報が欲しいはずです。

つまり、上2つの実行ではnumbers.select do ~ endという式に対してpメソッドを用いて出力しようとしていますが、思っている通りの結果が返ってきません。

どうしてでしょうか?

これは {}よりdo ~ endの方が結合の度合いが強く、プログラムが以下のような解釈をしてしまっているせいです。

   ↓第一引数           ↓pメソッドに渡しているブロック
p(numbers.select) do |num| num.even? end
  • p メソッドの第一引数がnumbers.select
  • pメソッドにブロックを渡している(do ~ endの間)

このようなことを防ぐには最初に示した例のように、まず変数に代入し、その変数に対してpメソッドを使います。

even_numbers = 
  numbers.select do |num|
    num.even?
  end

p even_numbers
=> [2, 4]

メソッドチェーンの途中の結果を確認したい

メソッドをつなげて少し複雑な処理を1行でやってる(メソッドチェーン)場合はデバッグが面倒になります。 例えば、以下の例を見てみましょう。

names = ['sasaki', 'yamada', 'suzuki']

# メソッドをつなげて少し複雑な処理をしている
names.map{ |name| name + '!' }.map(&:upcase).sort.reverse

# 見やすく整えるとこんな感じ
names
  .map{ |name| name + '!' }
  .map(&:upcase)
  .sort
  .reverse
=> ["YAMADA!", "SUZUKI!", "SASAKI!"]

次の4つのことを行っています。

  1. 名前に!という文字列を連結
  2. 大文字にする
  3. 名前を昇順に並び替える
  4. 名前を降順に並び替える

このコードは正しくプログラムが動くためデバッグする必要はないのですが、もしプログラムが動かなかった場合どこのメソッドまでの処理が正しくて、どこのメソッドからの処理が間違っているのか問題の切り分けが難しい場合があります。

そんなとき、tapメソッドを用いるとメソッドチェーンの途中を確認するのに便利です。

names = ['sasaki', 'yamada', 'suzuki']
names
  .map{ |name| name + '!' }.tap{ |x| p x } # tapメソッドを追加
  .map(&:upcase)
  .sort.tap{ |x| p x } # tapメソッドを追加
  .reverse

=> ["sasaki!", "yamada!", "suzuki!"] # map{ |name| name + '!' }までの処理結果
=> ["SASAKI!", "SUZUKI!", "YAMADA!"] # sortまでの処理結果

るりまによるとtapメソッドは以下のように書いてあります。

self を引数としてブロックを評価し、self を返します。 メソッドチェインの途中で直ちに操作結果を表示するためにメソッドチェインに "入り込む" ことが、このメソッドの主目的です。
https://docs.ruby-lang.org/ja/latest/method/Object/i/tap.html

メソッドチェーンで処理が連なっているときはぜひtapメソッドを使ってみてください。

プリントデバッグしたけど見にくいなー

次のコードをみてください。

users = [ {id: 1, name: '太郎', address: '東京', age: 30 }, {id: 2, name: '次郎', address: '大阪', age: 20 }, {id: 3, name: '三郎', address: '名古屋', age: 10} ]

配列の中にハッシュが入っているなど、データが入れ子になっているとき、普通にデバッグしても見づらくよくわからない時があります。そんなときはppメソッドを使うと便利です。

p users
=> [{:id=>1, :name=>"太郎", :address=>"東京", :age=>30}, {:id=>2, :name=>"次郎", :address=>"大阪", :age=>20}, {:id=>3, :name=>"三郎", :address=>"名古屋", :age=>10}]

# 適度に整形して出力される
pp users
=> [{:id=>1, :name=>"太郎", :address=>"東京", :age=>30},
=> {:id=>2, :name=>"次郎", :address=>"大阪", :age=>20},
=> {:id=>3, :name=>"三郎", :address=>"名古屋", :age=>10}]

ppメソッドが見やすいように整形してくれていることがわかります。

【番外編】プリントデバッグ地獄に立ち向かう

プリントデバッグに慣れてくると、ある病を患ってしまいます。そう、「なんでもとりあえずpつけておく病」です。

この病は深刻で当時の僕をかなり苦しめました。pメソッドを使いすぎて本当に知りたい情報が埋もれてしまうのです。(不必要なpメソッドは消せばいいのですが、、、)

そんな時に便利なのが次の方法です。以下のコードはとりあえず用意したコードなので、意味がわからなくても大丈夫です。

def dot(moras)
  moras.map do |mora|
    p square = Array.new(6) { '-' }
    first_char, second_char = mora.start_with?(/[WY]/) ? mora.chars : mora.chars.reverse
    p first_char
    p second_char
    p FIRST_CONVERSION_TABLE[first_char]&.each { |i| square[i] = 'o' }
    p SECOND_CONVERSION_TABLE[second_char]&.each { |i| square[i] = 'o' }
    p square
  end
end

これは、、、やってしまっています。pメソッドが多すぎてプリントデバッグ地獄が起こっています。

例えば変数squareの中身が知りたいとき装飾をつけることで目立たせることができます。

def dot(moras)
  moras.map do |mora|
    p square = Array.new(6) { '-' }
    first_char, second_char = mora.start_with?(/[WY]/) ? mora.chars : mora.chars.reverse
    p first_char
    p second_char
    p FIRST_CONVERSION_TABLE[first_char]&.each { |i| square[i] = 'o' }
    p SECOND_CONVERSION_TABLE[second_char]&.each { |i| square[i] = 'o' }
    p '=' * 30  # 装飾追加
    p square
    p '=' * 30 # 装飾追加
  end
end

'====== ...'で挟み込んでいるので欲しい情報がわかりやすくなりました。

$ ruby sample_code.rb
["-", "-", "-", "-", "-", "-"]
"A"
nil
[0]
nil
"=============================="
["o", "-", "-", "-", "-", "-"]  # 本当に知りたかったsquareの中身
"=============================="
["-", "-", "-", "-", "-", "-"]
"A"
"K"
[0]
[5]
"=============================="
["o", "-", "-", "-", "-", "o"]  # 本当に知りたかったsquareの中身
"=============================="

とはいえ、不必要になったpメソッドはこまめに消しておきましょう。

【番外編】紛れ込むpメソッドを探すために。

一生懸命デバッグをしプログラムが思い通りの挙動をするようになりました。あとは提出するだけなのですが、デバッグ用に差し込んだpメソッドがどうやら紛れ込んでいるみたいです。 VScodeなどのIDEを使っているなら検索が使えますので、検索をして探し出しましょう。ここではVScodeので探してみます。

class Product
  TAX_RATE = 1.1
  
  attr_accessor :name, :price
  
  def total_price
    p price * TAX_RATE
  end

  def format_price
    "#{total_price}"
  end
end

上のコードからpメソッドを探し出したいとします。見たらわかりますが何百行もあると思ってください。検索をかけるにはVScodecommand + fを押します。

f:id:haruguchi_yuma:20211214100740p:plain

なんとpとつく単語が7つも出てきました。

そんなときは正規表現を使って\bp\bと検索してください。そのとき、正規表現を有効にするために.*のマークをonにします。

f:id:haruguchi_yuma:20211214100923p:plain

\bは単語の境界を表すメタ文字(特殊な役割の文字)でpだけにマッチするようになります。

最後に

いかがでしたでしょうか?プログラムが動かないとき、つい頭で原因を探ってしまいがちですが、実際に1つ1つプログラムの実行結果を調べることで解決に近づくのでぜひ使ってみてください!