理論系の研究室に配属された、プログラム経験の浅い人に説明する際の補助資料。
いつも追記中。
自分が書いたコードを、自分以外(未来の自分も含む)が覚えていると思っている
自分が書いたコードは、書いた直後は自身でよく記憶しており、構造やコードの目的を明瞭に理解し、用意に所望の個所を特定することができる。自分以外(未来の自分も含む)も同じ状態にあると思い込んでいる。
まず動くものを作る というアプローチをとらない/知らない
コードを開発する際に、「期待通りに動くもの」が起点になっていない。あるいは、そこから多くの編集をしたうえで、動かない理由を探してしまう。原因と思しき候補が多すぎて、特定が困難を極める。
多くの編集を加えた後のコードを削除する必要はないが、それと「期待通り動く」コードを比較して、動くものがどの編集で動かなくなるのかを、明らかにする方が結果的に早いことがほとんどである。
Errorを読んでない
「うまくいかないんですけど」というコメントとともに、errorを読まずに丸投げするケース。
既存ライブラリの場合は、どういった条件でerrorが出ているか表示されているので、どういった内容にerrorが出ているのかををきちんと確認する。よくあるケースでは、配列の形が誤っていたり、実数引数のみが許される個所に複素数が代入されていたり 等の誤り。
自身で書いた関数/サブルーチンの場合は、該当のerrorがコードのどの行数のものかを確認する。python等スクリプト型言語の場合は、これで該当の個所が特定できる。Fotran/C++等、コンパイルする言語の場合は、tracebackを入れたり、たくさんのprint/write文を入れて、どこまでコードが動いたのかを確認する。
Errorの意味がいまいちよくわからない場合もある。そういう時は、該当の文章をコピペして、検索して、対処法を探したり、その意味を理解することに努める。
コードのテスト不足
通常、既存ライブラリを呼ぶ場合、自身で書いた関数/サブルーチンのテストを行う。その際の、テスト不足に関連したあるある。
ここでは、関数/サブルーチンをそう指定して、形式的に入力を$x$, 出力を$y$として、
$$
y=f(x)
$$
という構造を想定する。
正答が確認できる入力と出力でテストがされていない
まず、テストをする際には、入力と出力のペアとして、正答 $(x^* , y^* )$ が分かっているものについて、確認をする。
例えば、出力が$N$次元のベクトルなら、
$$
\mathrm{error} = || y^* - f(x^*) ||
$$
を評価して、その値の大きさが十分小さいことをもって、関数/サブルーチンの妥当性を評価できる。
この値がどれくらい小さくなるかは、関数$f$の性質次第で、理想的には入力が倍精度の数字なら、$10^{-15}$程度の大きさになる。
正答が分からないケースで、出力が何となく変
結果が同じになる状況で独立に答えを検証する
例えば、時間発展のコードでは解法によって一般に答えが異なる。しかし、通常時間発展のタイムステップが小さい極限で収束性を確認ができる場合は、異なる実装が同一の答えを導くことが期待できる。バグの入り方が偶発的であったり、実装の具体的な形に依存して導入される場合、ことなる実装で全く同様の誤りを導くバグが混入される確率は極めて小さいと期待される。複数の実装を試し、それらの整合性を確認することで自身の実装の正しさを確認することができる。
複数の実装が同じ答えを導き、それでも出力が妙な場合、実装の差には依存しない共通の要素(例えば量子系の時間発展であれば、ハミルトニアンだったり、外場の形だったり)がおかしい可能性が高く、問題点の個所を絞ることができる。
Warningを読んでいない
Warningが出ているが、一応プログラムは動き、何かしらの答えが出ているケース。
解決が極めて困難/コスパが悪い/そもそも問題ではないケース
Warningは度々極めて微妙な内容になる事もある。解決は出来るだけした方が良いが、様々な観点からもっともらしい答えが得られている確認ができた場合は、とりあえずWarningは出ているままだが、気にせず計算を進めるという選択肢もある。コスパの問題。
読んだ関数の中でさらに深部で呼ばれた、アクセスしにくいコード/プログラムからのWarning
関数は一般に多くの関数を参照していて、時に何回層も深いところでlegacyコードで書かれたプログラムがリンクされていることがある。こうしたコードは現代的なセンスで書かれていなかったりして、warningの内容もいまいちわかりにくいことがある。また、そのコード自体にアクセスできないので、該当箇所を特定することが極めて困難な事がある。あまりにも大変な場合は、しばしば特定をあきらめる。
単なるおせっかいなWarning
反復解法等で、「勾配ベクトルのノルムが小さすぎるために、反復回数に達していないけれど、解はそれ以上更新されていないよ」という事を教えてくれているWarningが出たりする。(言ってしまえばただのおせっかいである。)このケースであれば、勾配ベクトルが小さいという事は、それすなわちその解法の限界、あるいは数値近似解が求まったことを意味しているので、こうしたWarningには気にせず計算を遂行すればよい。
変数の型の整合性をきちんととっていない
浮動小数点か、整数か、あるいは文字列か といった変数の型の整合性をきちんと確認していないケース。特にpython等のスクリプト型言語で顕在化する。(コンパイルが必要な言語では通常型由来の問題は、コンパイル時にerrorで止まるので。)
少し気を付けないといけないのは、浮動小数点のケースでも、arrayなのか、スカラー値しか許さないのか、arrayの形が(N)なのか(N,0)なのかあたりも、気をつけないとエラーの原因になる。
コードを書き換えるときに、既存コードを残さずに書き換えてしまう
既存コードに不足があったり、バグが混入していたりして、コードを書き換える必要がある。その時に、古いコードを書き換えたり、消したりして、既存コードを修復不可能な形で編集を進めてしまう。問題の再現性を確認するうえでも、問題が完全に解決するまでは既存コードは残しておく方が良い。
例えば、既存コードはコメントアウトして直上か直下に所望のコードを書き足せばよい。コメントアウトの場所を変えることで、すぐに問題のコードに戻ることができる。正しい実装が出来たと確信したところで、既存コードは削除すればよい。
他には、編集箇所と同等の機能を有する関数のコードを追記して、既存コードを使うか、追記した関数を呼び出すかを切り替えるようにしておけばよい。
コードのテスト頻度が低い
コードに多くの編集を加えてからテストをして、エラーの解決に取り組むことになる。変更箇所が多岐にわたるために原因究明が困難になる。
頻繁(程度問題だけど)にコンパイル/ビルド/テストをして、問題個所が容易にわかるようにするべき。
コードのバージョン管理がされていない
単一のディレクトリでコードを開発していて、つねにmasterを編集している。コードを不可逆な形で編集しているため、正しく動いたコードを参照することができない。
Gitあるいは、コードを大きく変える際に日付ディレクトリ/バージョン毎のディレクトリを変える、その日付のコード群をzip, tar.gzに纏めて、どこかにアーカイブしておく という程度の対策は取っておくべき。
コードの編集にあたって、記述事項の局所化が図られていない
すでに動作確認が取れている二つのコード/関数を共同させるときに、変数の型や与え方に齟齬がある際、編集内容が局所的なところで閉じずに全体にわたってしまう開発をしてしまう。
理想的には、二つのコード/関数の間の齟齬を解決するコード/関数を開発することで、すでに動いている部分に編集を加えないようにコードを書いた方が、通常能率的である。こうすることで、問題の個所が局所化されて、デバッグ等のコストが削減される。
記述したコードが関数化/サブルーチン化されていない
メインのコードで様々な開発をした後に、定型化された機能が関数化/サブルーチン化がされていない。そのため、機能追加や変更をした際に変更箇所が多岐にわたっており、保守性が悪い。例えば、jupyter notebookで開発をしているようなときに陥りがちになる。複数のjupyter notebookに同じコードが何度も登場していて、全体を書き換えようとした際に、手間が著しく増える。
求まった数値を確認していない
計算の経過をプロットして確認したりする際に、プロットされた結果だけを見て判断しており、数字が実際にどういった数字になっているのかを確認していない。
例えば、プロットされた領域が妙になっていて、結果が変に見えていても、どこに原因があるのか(最悪の場合プロットの問題ではなく根本的に数値がおかしい可能性がある)判断できない。
言語/仕様/手法/実装の問題点や特徴を捉える際に、解像度が粗い
例えば、「Aという言語で書かれたBという手法を実装したコードCに問題がある」という知識を得たときに、問題の原因がどこに原因があるのかの追及が甘い。A, Bいずれかに問題があるのか、Cに実装している陽なコードが悪いのか、AとBが組み合わさったときはいつでもそれが問題になるのか を切り分けてとらえられていない。
バグあるある
配列/変数の初期化をしていない
時間発展等の反復計算をするうえで初期条件が適切に導入されていないケース。スカラー値であれば、定義の段階で初期化し、配列も初期化可能な定義方法を使う。