プログラミングテクニックというか、コツというのはあります。本章ではプログラミングで楽をするための先人の知恵をいくつか紹介します。
この節で紹介する「動くことを確認する」ということは意外とバカになりません。「たぶん動くだろう」の状況でコードを読み書きしても、実際に動かそうとしたときにエラーがでるようでは意味がありません1。これは慣れている人でもときどき原因不明のエラーや更新してるのに反映されてなくて数時間が溶けるということはよくある話です。
CLIツール2であれば、コマンドラインで実際に起動してみて、想定動作をするか?Webアプリであれば、ブラウザでアクセスできるかどうか?を確認します。
たとえば、文字列を変更すれば、実際にその文字列を使ってる部分、CLIツールならコンソールやファイルに出力される文字に違いが出るか?Webアプリであれば、ブラウザでアクセスしたときに表示が変わるか?を確認します3。
これは慣れない環境で少し特殊なことをするなどした時にはよくハマります。たとえばmacOSを日頃使っている人が remote ssh で RaspberryPI(ラズパイ)でアプリ開発をしようとしたら、watchモード(ソースコードの変更を検知して再コンパイルの仕組み)が働かない、などといったことがありえます。
Dockerも便利ですが、よくハマる可能性があるヤツです。
また、キャッシュの類いが悪さをすることもよくあることです。
yarn.lock
やpackage-lock.json
を消したり、node-modules
ディレクトリを消して、インストールからやり直すとうまくいくというのはよくあるパターンです。特に妙なテクニックを自分で使っている、あるいはライブラリやフレームワークが使っている場合に起こります。
ただし、この手段は再インストールに、ネットワークの帯域が必要になるため、出先で作業をしているとダウンロードが終わらない悲しみに暮れる可能性もあります。また、場合によってはCPUやメモリ、ディスク負荷が掛かることもあります。
また、コマンドラインでパッケージをインストールしたもののpackage.json
に反映してなくて困ることもあります。
似たようなものとして、npmパッケージのバージョンによって細かい挙動の違いがでることもあります。Node.jsのアプリはnpmパッケージに依存しすぎ問題があり、意外と複雑に絡み合って意味不明な挙動をすることがあります。
Node.js本体のバージョンに左右されることもありますし、グローバルにインストールしているものによることもあるでしょう。
対策としては、anyenv + nodenv や Nodist(Windows)などを駆使してバージョンをある程度新しめに更新しておく、グローバルにはあまりソフトはインストールしないなどです。
最初はあまり考えなくてもいいですが、いつかの段階で必ず、最小限の環境については知るべきです。
典型的な事例としてはボイラープレートを使って環境を構築している4場合です。
「どれを削るとどの機能が消失するのか?」を探求しておくべきです。
理由としてはプロジェクトが進めば進むほど、最小限についての確認が難しくなってしまうことと、「この機能、どう考えてもいらないんだけど、なぜ消さないんだろう?」という疑問がつきまとうことになり、大抵それらは技術的負債につながるものです。
デバッグをするための方法や仕組みには色々なものがあります。
古典的デバッグ方法と呼ばれるものですが、未だによく使われるものです。使い慣れればデバッガを駆使した方が効率がいいこともありますが、値を常に垂れ流すというのはそれはそれで便利なことも事実です。
console.log(hoge) // hogeという変数の中身をコンソールに出力する
printデバッグが役に立つのは主に2つの事例です。まずは、対象のコードについて理解が浅いときです。 たとえば誰かからソースコードを引き付いたときや、OSSの挙動を確認、解析するときです。
何をやっているのかわからないのであれば、ソースの随所に変数の中身を確認するデバッグコードを挿入したり、今、どこに制御が遷ったか?を確認する為のデバッグコードを挿入します5。
function nazoFunction(...params) {
console.log(`enter nazoFunction ${params}`)
// なぞのコード群
console.log('leave nazoFunction')
}
というような感じです。
呼び出し元の情報を取得できることもあるので、そういった場合は、よく呼び出される関数に、呼び出し元の表示機能を付けてみるというのもありです。
対象のコードへの理解はともかく、実現したいことに対して理解が浅いときもあります。 これはたとえば自作コードなんだけど、自分が利用するAPIについて熟知しているわけではないときです。
あるエラー状態だと、どんなエラーオブジェクトが帰ってくるのか?あるいは何か他のオブジェクトがあるのか?などといった情報は、ドキュメントが豊富なプロダクトでも、意外に欠けている情報だったりします。
こういったものに対処するためには、実際にAPIを叩いてみるのが最適です。
もっともAPIを叩く為に、ブラウザのデバッグコンソールを使う、Node.jsのReplを使うなどのテクニックもあるため、必ずしも、printデバッグでやる必要があるとは限りません。APIを叩くのにいくつかの手順が必要な場合なんかは、print文を仕込むという泥臭いやり方でやることもあるでしょう。
別名として、契約的プログラミングと呼ばれることもあります。大抵の言語にはassert
と呼ばれる仕組みがあります。JavaScriptで有名なものとしては、power-assert
と unassert
です。
const assert = require('power-assert')
const a = 1
const b = 2
assert(a === b)
このようなコードを書いたとき、標準的な assert
では、単にエラーが出て落ちるだけということが多いですが、power-assert
を使えば、assert
の中の演算式を細かく分解した上で、どれがどういう値になって失敗するのか?を詳しく教えてくれます。
AssertionError [ERR_ASSERTION]: # test.js:6
assert(a === b)
| | |
| | 2
1 false
[number] b
=> 2
[number] a
=> 1
これも古典的なデバッグ方法ですが、何かしらバグが生じてる付近をコメントアウトして、バグが生じてる直接の場所を探るテクニックです。
動作がおかしくなるのはわかってるが、それが生じてる場所が分からない場合、少しずつ箇所を特定していく必要があります。
少なくともここではないという場所を、順次潰していくのです。
場合によっては二分探索的にやれることもあるでしょう。たとえばある関数でバグが生じていることが判明している場合、その関数の半分をコメントアウトしてバグが生じないか?
もっとも、半分をコメントアウトすると、そもそも動かないというケースも少なくありません。
C++を利用する場合、GDBとVisual Studio という2つの強力なデバッガを使える。
プログラムで実行している途中、前述の通りprintデバッグは取っつき易くて使いやすい。 一方で、コードを修正しないと値が表示できないため、C/C++やJavaと行った、コンパイルが必要な言語の場合、変数の中身を覗きたくても、すぐに覗けないことがある。 この際に、実行途中のプログラムの変数の内容を覗くことができるのが「デバッガ」の便利な機能である。
Visual Studio の場合、まずは「デバッグ実行」を選択するところから始まる。 通常の「実行」と「デバッグ実行」の違いは、このデバッガ機能の有無を指します。 言い換えると、通常の「実行」はファイルエクスプローラからプログラムをダブルクリックして実行することと等価です。 デバッグ実行すると、デバッガがプロセスの中を覗く分、実行速度は低下します。
また、一般的にVisual Studioでビルドする場合は「デバッグモード」と「リリースモード」の2種類が提供されます。 デバッガの機能はデバッグモードでしか有効ではなく、リリースモードではデバッガの機能が使えないか、著しく制限されます。 リリースモードで「デバッグ実行」の組み合わせは速度も遅くなる上にデバッガの機能の恩恵にも預かれないので、利用するモードに注意しましょう。
さて、肝心のプログラム内の変数を覗く場合、基本的にはプログラムを停止させる必要があります。 ただし、完全に停止させるのではなく、変数を見る間だけ、「一時停止」させるのが望ましいです。 これを実現するための機能がブレークポイントです。 デバッグメニュー内の「ブレークポイント」で任意の行にブレークポイントをしかけることができます。
「デバッグモード」で「デバッグ実行」で「ブレークポイント」を設定してあり、プログラムが実行中にブレークポイントに到達すると、デバッガがプログラムの実行を一時停止します。 この状態で初めて変数の中身を覗くことが出来ます。
変数はウィンドウに「自動」「ローカル」「ウォッチ」の3種類のウィンドウがあらわれ、それぞれタブで切り替えることができます。 それぞれのタブで表示される変数が違い、目的に合わせて使い分ける必要があります。
- 自動: 直前の行と、現在の行でアクセスしている変数。直前の行で変更が行われた変数は赤字で強調表示される。
- ローカル: 現在のスコープでアクセスできる変数を表示する。ローカル変数、同じクラス内のメンバ変数など
- ウォッチ: プログラム起動直後は空っぽで何の変数も表示されない。覗きたい変数を登録しておくと、中身を随時更新して表示してくれる。
「何がおかしいのか全くわからない」という不具合を解決するのに便利なのが二分探索法です。
プログラムを書いていると、時としてどうにも説明のつかない現象が発生することがあります。どう考えても動くはずのコードが動作しなかったり、どう見ても正しいコードなのに意味のわからないコンパイルエラーになってしまったり...
「これ言語のほうがおかしいんじゃないの?」「パソコンが壊れてる?」「幽霊がいる?」といいたくなる気持ちはわかります。けれども、パソコンも言語も、世界中の人が使って修正が繰り返されてきた堅牢なものですから、ほとんどの場合間違っているのはプログラマの方です。世界がおかしいと疑うよりは、まずは自分の書いたコードがおかしい可能性を疑ったほうが、たいていの場合早く問題を解決することが出来ます。
問題が起きる箇所が大まかにわかったら、ソースコードのうち半分を削除(コメントアウト)してみましょう。まるまる半分消してしまうとシステムが動作しなくなったり、コンパイルすることができなくなってしまうかもしれませんが、たいていの場合、「この行はとりあえず外しても動く」「計算をしなくてもとりあえず数字を入れておけば動く」というような具合にすれば、はずすことのできる行がたくさん出てくるはずです。 その状態でもう一度動かしてみましょう。問題が再現しましたか?
問題が再現する場合、今消したコードは問題とは関係がなくて、残っているコードのどこかに問題があることがわかります。この場合には、残っているコードのうち、削除(コメントアウト)できそうなところをさらに探して外してみましょう。 逆に問題が再現しない(直った)場合には、消した方のコードに問題があるはずです。この場合には、消したコードのうち一部を復元して、もう一度実行します。
これを繰り返すと、問題が起きる箇所が徐々に絞り込まれていきます。最終的には、この一行があると問題が起きる、消せば直る、という状態まで追い込むことができるはずです。ここまでくれば、さすがに問題を見つけることが出来ますよね。
ソースコードの半分をコメントアウト、問題が再現したらさらに半分をコメントアウト、という作業を繰り返すと、そのたびに問題が起きる箇所を半分に絞り込むことができることから、この手法は「二分探索」と呼ばれています。
様々な手法を繰り出しても問題が特定できない場合には、他の人に聞くのが有効です。 同僚やチームメイトでもいいですし、インターネットには様々なコミュニティがあるので、そこで聞くのも良いでしょう。
ただこの場合、まず何が問題であるかを相手に理解してもらう必要があります。一緒に仕事をしていたり、画面を共有してみてもらえる場合には、今起きている問題をそのまま見てもらうことが可能な場合もありますが、インターネットで人に頼る場合、相手はあなたのパソコンが見えないので、できるだけわかりやすく問題を伝える必要があります。
このために必要なのが、サンプルの作成です。
あなたが作ろうとしているシステムは、様々なファイルがあり、セットアップに時間がかかります。そういった要素を削ぎ落とした、最小限のサンプルを作りましょう。
Rubyやpyhtonなどの言語では、対話型シェルというものがあります。入力するたびに結果が表示されるこの画面上で再現できる程度まで小さくなれば、誰でもすぐに問題を再現することができるので、答えを得やすくなります。
Ruby on Railsのようなライブラリや、コンパイル型の言語の場合、デフォルトで作成されるソースコードにこのファイルを貼れば問題が再現します、という形が理想的です。
このサンプルは、他人に問題を理解してもらうために作るものなのですが、実のところ、これを作るとたいていの問題は聞く前に解決します。 最小限のサンプルを作ろうとすると、必然的に無関係な要素を削除することになるし、問題をもう一度最初から考え直すことになるので、とちゅうで「あ、これか」という具合に自分で問題に気づくのです。そういう意味では、最小限のサンプルを作ること自体も、デバッグ手法の一つと言えるでしょう。
サンプルを作成すると自分で問題に気づけると述べましたが、これとよく似た手法が「ベアプログラミング」です。よく似た名前で「ペア(pair)プログラミング」というのがありますが、ここで述べるのは「ベア(熊)プログラミング」です。「ラバーダッキング」とも言います。
ぬいぐるみデバッグ、テディベア効果とも呼ばれるこの手法では、クマのぬいぐるみを用意します。クマでなくてもよいのですが、伝統的にクマが良いとされています。
ぬいぐるみが用意できたら、彼に向かって問題を説明しましょう。恥ずかしがってはいけません。ぬいぐるみは何も知らないので、自分が今何をしようとしているのか、どういう問題が起きているのか、それに対してどういう調査をしたのか、その調査が駄目だった理由、一つ一つ丁寧に説明してあげてください。
やっているうちに、「あ」と自分の勘違いに気づくときがやってきます。そうなったら、ぬいぐるみに感謝してプログラミングに戻りましょう。
同じことは、家族やペット、同僚などでも出来ますが、クマのぬいぐるみはどれほど時間を使っても怒らないし、問題がわかったら即座にやめてもなんの問題も起きないという点でとても優れているとされています。
Footnotes
-
すでに開発環境の章で、貴方が開発するための環境は、本番の開発と同じになっているはずなのでそれを前提とします。 ↩
-
コマンドラインから起動するツールのことです。基本は文字を入力して文字を返すアプリが主です。 ↩
-
TDDには似たようなテクニックがルール化されています。最初は必ず失敗になるエラーを書くというのがそれです。エラーが出ることを確認し、修正したらそのエラーが無くなるという工程によって、変更を正しく反映していることを確認できるのです。 ↩
-
Node.js環境なら
create-react-app
やvue-cli
で作ったディレクトリや Ruby on Railsなら、rails new
で作ったディレクトリです。 ↩ -
筆者はよく`console.log(1)とかconsole.log(2)みたいなコードを埋め込むことがあります。 ↩