REPLとデバッガを使って鮮やかにバグを解決する
この記事はQiitaで書いた記事の転載です。
技術系記事をはてなブログに書いていこうかなと思い、平行利用してみます。
qiita.com
本記事では、RailsでのWebアプリ開発で培ったREPLを使ったデバッグについて書きます。
はじめに
基本的にはRails利用者向けに書いていますが、Railsの機能を利用している具体的な箇所をご自身が利用されているフレームワークに当てはめれば、Railsを使っていない方でも役に立つかもしれません。
話の内容としては以下の2本立てです。
- pry-byebugってなに?
- REPL駆動デバッグをはじめよう Railsに興味がない人は 2 だけでも読んでいただければと思います。
pry-byebugってなに?
pryとbyebugという2つのGemを組み合わせて使えるようにするライブラリです。
pryとは高機能なREPL(インタプリタとの対話環境)です。
一方で、byebugはブレークポイントをコード上に打ってステップ実行をしていくデバッガです。
pry-byebugはこの2つのGemを組み合わせ、byebugの機能を対話的に実行できるようにするものです。
具体例を見せてくれ
controllerにbinding.pry
を置いて実行してみます。
class BullteinBoardController < ApplicationController def show binding.pry # pryのブレークポイントの置き方 @board = BullteinBoard.find(params[:id]) end end
ここでconsoleを確認してみると以下のような画面が表示されます。
pry-byebugはこの画面でプログラムと対話をしながら、デバッグをしていきます。
From: {省略}/app/controllers/bulltein_boards_controller.rb @ line 4 BullteinBoardsController#show: 2: def show 3: binding.pry => 4: @board = BullteinBoard.find(params[:id]) 5: end [1] pry(#<BullteinBoardsController>)> step # 直後のメソッドコールにステップインします From: {省略}/vendor/bundle/ruby/2.4.0/gems/actionpack-5.1.5/lib/action_controller/metal/strong_parameters.rb @ line 1067 ActionController::StrongParameters#params: 1066: def params => 1067: @_params ||= Parameters.new(request.parameters) 1068: end [1] pry(#<BullteinBoardsController>):1> request.parameters # 呼び出せる変数や関数を呼び出すとその結果が表示されます => {"controller"=>"bulltein_boards", "action"=>"show", "id"=>"16"}
詳しい使い方はk0kubunさんの『今更聞けないpryの使い方と便利プラグイン集』におまかせします。
2. REPL駆動デバッグをはじめよう
REPL駆動デバッグとは?
私の造語で、REPLを使ってデバッグをしていくことを指します。1
エラーメッセージと発生箇所だけで原因が特定できる単純なエラーであれば、対処は非常に楽です。
しかし、バグやエラーの原因が発生箇所とは遠い彼方のモジュールにあったり、利用しているデータが壊れていることが原因だったり、はたまた複合的な要因だったりすることもあります。
そういう状況でのバグ対処では、まずは原因を切り分けて発生源を特定していきますよね。
REPL駆動デバッグは、その作業をREPLを駆使してサッと解決してしまおうというものです。
REPLとデバッガでバグを絞り込むテクニック
① REPLで正常な挙動をチェックする
バグを修正するためには、まずはゴールを確認する必要があります。
そのため、「どこをどうすればいいのか」の情報を得る必要があります。
最初にバグが発生している部分に焦点を当てて、本来あるべき正常な挙動と現在発生している異常な挙動を確認します。
そして、その部分をコード上で確認し、関係しているロジックのどの値がどうなっていればあるべき挙動になるのかを調査します。
それさえできれば、その値が作られたロジックを遡って、挙動の確認を繰り返していくことで芋づる式にバグの原因にまでたどり着けるからです。2
こうして、それぞれの部分であるべき動作を確認する作業では、コード上で思いつく限りの可能性を実験的に試していくことになると思います。
そんなシチュエーションでは、小さな修正とコードの再実行を繰り返すことになります。
そのため、コードを実行をしなおす時間すら惜しく思えてくるのではないかと思います。
そういうときにこそ、REPLが役に立ちます。
具体例
以下は、REPLを使って正常な挙動を確認する具体例です。
# 周辺のお店を紹介するサービスを運営しているという想定。 # LPのような特集ページで動的に生成しているリンク一覧が、仕様どおりの順番で並んでいないというバグがあった!という問題設定。 # 場面設定: Viewで実行している`StoreListLinks#generate`が出力しているリンクの順番がおかしい事がわかった class StoreListLinks def initialize(locations:, arranger:) @locations = locations @arranger = arranger end def generate # ... # @locations から Linkクラス的なモノのコレクションを作り、 # それを @arranger でいい感じに整形して返すロジック # @arranger は StoreListLink::RecommendedArranger と StoreListLink::MarketableArranger がある想定。[3](#class) # この関数の内部で binding.pry を使って調べていく end ... end
まずはじめに、#generate
の挙動を調べるため、メソッドの内部でbinding.pry
をおきます。
どこにおけばわからないときは、とりあえずメソッドの一番上に置くといいんじゃないかと思います。
binding.pry
で動作を止めた箇所で、正しい挙動を確認してみます。
このメソッドの結果に影響を及ぼしていそうなところをチェックしましょう。
【メソッド内をチェックするときに見ると良い箇所】
- 引数
- インスタンス変数
- 内部で利用しているメソッド・関数
ここで@arranger
の値が想定外のモノだったとします。
その場合、それがどんな値になれば正常な挙動をするのかをチェックしてみましょう。
...(pryのデバッグコードが並ぶ) [1] pry(<StoreListLinks>)> @arranger => #<StoreListLink::MarketableArranger:0x007ffedfeb5330> [2] pry(<StoreListLinks>)> # これは想定外のクラス! [2] pry(<StoreListLinks>)> # 仕様どおりであれば、StoreListLink::RecommendedArrangerが来るべきだった [4] pry(<StoreListLinks>)> # まずは、とりあえず正しい挙動を試してみよう [5] pry(<StoreListLinks>)> @arranger = StoreListLink::RecommendedArranger.new(...) => <StoreListLink::RecommendedArranger:0x007ffedfe27238> [6] pry(<StoreListLinks>)> # つまり、REPLでインスタンス変数に本来来るべきクラスを代入してしまう [7] pry(<StoreListLinks>)> self.generate # そして、メソッドの実行結果を試してみて正しい挙動になることを確認してみる
このようにREPLを使うと、@arranger = StoreListLink::RecommendedArranger.new(...)
のようにインスタンス変数や引数を直接書き換えて、挙動をチェックできます。
このようにして、REPLを使って試行錯誤をすることで、原因の切り分けを手早く終わらせていきましょう。
② 探索範囲を徐々に狭めて二分探索していく
個別のクラスや関数の中でバグの原因を調査をするとき、まずは関数やクラスの結合点を調べると良いです。 バグの原因が外にあるのか内から生まれたのかを確認して、探索範囲を絞っていきます。 そうして、範囲を徐々に絞り込んでいくことで、関係のない箇所を調査して時間を浪費してしまうことを避けられます。
【関数やクラスの結合点の主な例】
- クラスのイニシャライザ
- メソッド・関数の引数
- メソッド内で利用されているインスタンス変数
具体例
以下に軽い具体例をあげます。
クラスのイニシャライザ(#intialize
)で代入されてる値が不正でないかをチェックしてみる、という例です。
# コードは先ほどと同じ例です # 以下のコードで2つのケースを考える # 1. `#initialize`の時点で`sort_key`が間違っていることがわかった場合 # 2. 特に引数の値の間違っているものがない場合 class StoreListLinks def initialize(locations:, arranger:) @locations = locations @arranger = arranger binding.pry # ここで止まるので @locations と @arranger の値を確認してみる end ... end
#initialize
の時点でarranger
が間違っていることがわかった場合
引数の時点で不正な値になっている場合は、現在のクラスの外にバグの原因があると推測できます。
さらに言えば、このクラスを利用している部分を調べる必要があるでしょう。
引数に渡している値を調べて、バグの原因をさらに辿っていくと良いです。
特に引数の値の間違っているものがない場合
不正な値が入っているように見えなければ、そのクラス内でバグが作られている可能性が高いと判断できます。
クラス内のロジックにバグが潜んでいて、それが原因で想定外の結果が生み出されていると言えそうです。
そのため、バグの発生箇所で呼び出しているメソッドのロジックに関係している部分(プライベートメソッド、インスタンス変数など)を確認してみましょう。
このように結合点を確認して、バグを調べていくことでより早くバグの原因にたどり着けるようになります。
以上です。
ソフトウェアエンジニアならば、普通にやっていることを言語化しただけかもしれませんね。
ツッコミや提案があったらよろしくお願いします。
次に学ぶプログラミング言語の学習には『Koans』がとても良さそう
前置き
Pythonを読めるようになろうと思い、Pythonの勉強をしていました。
というのも、海外の記事を読むと、サンプルコードや実装例にPythonが使われることが多く、読めないことに危機感を感じていたからです。
そこで、Python Koans(GitHub - gregmalcolm/python_koans: Python Koans - Learn Python through TDD)を一通りやってみたところ、「これははじめて学ぶプログラミング言語の取っ掛かりにとても良いぞ!」と感じました。
Koansとは?
ざっくりといえば、言語の機能・シンタックス・ライブラリを学習するための問題集です。
落ちているテストを通すという形式で、問題に解答していきます。
Ruby Koans(Learn Ruby with the Edgecase Ruby Koans)が元で、そこから様々な言語に広がっていったようです。
Koansの語源は「禅の修行者が悟りを開くための課題」を意味する「考案」から来ています。
主語が大きいかもしれませんが、海外のエンジニアの方たちって禅が大好きだな、と改めて感じました。
Koansは”次の”言語学習の近道である
言語の入門本には「プログラミング」そのものの学習を含んでいることが多く、既に一つ以上のプログラミング言語を習得しているエンジニアにとっては簡単すぎることが多いです。
一方で言語機能を網羅した辞書のような本は、はじめたて言語の学習に読む本としては重すぎます。
Koansは入門本と辞書本の中間のような立ち位置で、他の言語を習得済みのエンジニアが次の言語を触るための橋渡しをしてくれる存在と言えます。
Koansは「問題を解く」ことを中心に進めていきます。
手を動かしながら、「この問題を解くためにはこうする必要があるだろう。この言語ではどうやればいいんだろうか。(ググる。)」という感じで試行錯誤しながら進めることになります。
そのため、「手に覚えさせる」ように学習することができ、ただただ本を写経するよりも記憶に残りやすかったです。
Koansを一通りやり終えている頃には簡単なプログラムを書けるようになっている状態になっており、サクッと終わるにも関わらず力がつく良いコンテンツでした。
「新しい文章力の教室」を読んだ。
私は文章力の低さや書くことに対する心理的な抵抗感を持っていることを、ずっと課題に思ってきました。
この抵抗感を生み出しているのは文章を書く機会が少ないからだと考えて、書くことの苦手意識を克服するためにブログをはじめました。
しかし書きたい内容があっても、どう書けばいいのかがわからずに困り果てていました。
よしんば書けたとしても公開後の文章を読み返すたびに、誤字脱字や文章のくどさに気がついて恥ずかしい思いをよくしています。
そこで体系的な文章の書き方をインプットして、書けない状況から脱却し質の高い文章を書きたいと考えてこの本を手に取りました。
この本自身が良い文章のお手本と言っても過言ではない
この本はカルチャーニュースサイト「ナタリー」で、文章教室「唐木ゼミ」にて筆者が講義していた内容を一般向けにした本です。
良い文章を「完読させる文章」と定義して、そこに至るためにどうすれば良いのかを指導してくれます。
この本自体が「完読させる文章」のお手本だと思えるほど、スッと入ってくる読み味でサクサクと読むことができ、本の内容に説得力を感じました。
ひととおりの基礎的な「文章の書き方」を学べる
本書をざっとまとめると以下の三点になります。
- 文章を書くためのフレームワーク「構造シート」
- 文章を磨くための校正のポイント
- 読ませる文にするためのTIPS
どれも納得感を感じるような内容で、すぐにでも実践したくなるものでした。
この本の内容を実践するのにあると便利だった道具
nuboard
持ち歩き用ホワイトボードです。
本の内容を実践すると、構造シートをたくさん書くことになります。
最初のうちは構造シートを手書きすることが推奨されており、書き捨てをしやすいホワイトボードがとても重宝しました。
この本の内容に関わらず、持ってると便利なアイテムです。
ちょっと実践してみた
読んで学んだことを思い出しながら、このPostで実践してみました。
最後まで読んでいただけたでしょうか。
少しでも読みやすい文章になっていれば幸いです。
もっと読み返したり実践を繰り返したりして、より良い文章力を身に着けていこうと思います。
「HTML5/CSS3 モダンコーディング」を読んだ。
これまでは、HTMLやCSSをBootstrapに頼ったり、とりあえずググってなんとなく解決したり、という危うい感じで書いてきました。
しかし、このままでは良くないと思い、フロントコーディングの基礎を得るために「HTML5/CSS3 モダンコーディング」を読んでみました。
フロントコーディングの流れを知ることができた
チュートリアル形式で0からHTMLとCSSを組み立てていくため、フロントコーディングの全体感を把握することができました。
実践的なだけあって、どこから手を付けていけばよいのか、どういう考えでどういうコードを書けばよいのか、そういう知見を体験しながら得られる本でした。
「思い通りの見た目」を実現するための実践的なテクニックを得られた
box-sizing
の使い方::before
・::after
の有効な活用方法position
の使い分けtransform
とtransition
を使って変化のあるデザインを実現する- ...etc
こうしたテクニックや知識を、必要になった時に必要な分だけ得ながら、サクサクと心地よく進めることができたのが良かったです。
この本を進めていく際に準備すると良いもの
emmet
Emmet — the essential toolkit for web-developers
HTMLを省略記法で書いて、その場でHTMLに変換してくれるツールです。
この本ではHTMLをたくさん書いていくため、いれておくと便利です。
多数のエディタにプラグインが提供されているため、お使いのエディタで探してみると見つかるかもしれません。
参考までにVimとEmacsのプラグインだけ貼っておきます。
browsersync
Browsersync - Time-saving synchronised browser testing
特定のディレクトリ下のファイルが更新された時に、ブラウザの更新をしてくれるツールです。
特に設定ファイルを書かなくても使えるツールだったため、今回のような静的コンテンツだけをコーディングしていく用途には適していました。
得られた気づき
写経の効用って、書き方の勘をつかむだけじゃなくて、ほどよくサボっていくために、界隈のツールセットを整えていけるところにあるんじゃないかと思った
— otakumesi🍞 (@otakumesi) 2017年12月24日
コードの写経って、なんだかんだで面倒くさくなるんですよね。
そのタイミングで、いい感じにサボるために界隈の便利ツールを探して導入したりしてました。
そして、写経が終わってる頃には便利ツールを利用できる環境ができあがっていて、さらに便利ツールの使い方をわかっている状態になっていて、「良さ(うまい言葉が見つからない)」を感じました。
写経には、そういう効果もあるんだなと気が付いて、改めて写経って良い勉強方法だなと思いました。