【デバッグ法1】 にじり戦法
昔を思い出して、かつてOmise Japan時代にカナダ人の老練エンジニアに学んだ手法を共有したいと思う。
できるエンジニアとそうでないエンジニアを大きく分けるものは以下である:
1. 数学的なかんがえかた(そしてアルゴリズム)
2. 経験値
3. ふだんどれだけ技術を勉強してるか
4. こころがけ
1. まず数学的な考え方については異論はあるまい、というところでしょうか。なんでGoogleとかいわゆる大手外資でコンピュータサイエンスの学位必須にしてたり(まあそれ相当、という建前があるとして)OYOの面接でアルゴリズムをがっつり突っ込まれるかというっとこれがエンジニアのパフォーマンスを決定的に決めるからだ。
アルゴリズムは、もうこれだっていう教科書があるので、それを買って読むべき。数学は避けて通れないから、外山滋比古さんの「β読み」ではないけど、とりあえずきちんと全部読むべきである。これ、やるのとやらないのでは天と地の差があり、困難にぶつかった時に聖書のようにあなたを助けてくれる。アルゴリズムの重要性についてはもう少し駆け出しの人のためにうまい比喩をつかうと、
例えば空手を10年やってる人がある日突然酔っ払いに絡まれて殴られそうになったが、秒で交わして捌けた、といったような感覚に近いので絶対にやるべきである。
2. 経験値 である。経験値に胡座を書いてはいけないしこの世界陳腐化のスピードが激しいのは確かではあるが、経験値は確かに助けてくれる。あのときこういう問題に直面して、こういうふうに解決した.... というものが増えてくれば継続は力なりであって必ずあなたを助けてくれる。
3. 逆の言い方をすれば技術に興味をもてない、とか自身のことを「職業エンジニア」だとか正当化しているような「輩YAKARA」は絶対に底辺エンジニアから這い上がれないので覚悟したほうがいい。つまり、例えばだが私のかつて一緒に働いたカナダ人エンジニアの場合だと(彼はDevopsだったが) Haskellがっつりやっていて、ハードウェアとかアセンブリあたりにもかなり造詣がありエンジニアの集まりにしょっちゅう顔を出していた。こういう「興味」とか「好奇心」というのは、2、3ヶ月では浅すぎて何も効果がみえない、と思いきや積み重なるとものすごいことになるのである。Stack overflowをメインにするのではなく、きちんと技術書は大量に購入して投資をすべきだし、そうでないと絶対に次のステップにはいけない。
4. こころがけ である。
このこころがけ、というのは私が「出来る」エンジニア(とくに外人)を観察していてみてきた統計みたいなものなので、ある程度信憑性はあると思う。では、こころがけの一つ、
「にじり戦法」
について話してみようと思う(前置きが長くなったな)、
ぼくたちエンジニアにとってもっとも辛いというかストレスフルなシーンというのは、
エントロピーの高い問題にぶつかったとき
である。もちろんこういうのを楽しめるマインドは根っからの優秀なのでコメントなしなのだが、基本的には多くのエンジニアがここでぶつかり、時間を浪費していく。例えば、
class Sample {
public someMethod(SomeStruct arg1, OtherStruct arg2, ElseStruct arg3) {
def arg1Copy = copyClass.copySomeObject(arg1)
List<Some> arg2Copy = ...
if(arg3.nasty == arg2Copy.size()) {
for(x in xs) {
for (y in x) {
object.domain.class(x, y, otherStruct).subscribe { a =>
List event = a.eventGet(otherCode, arg4, arg5, arg6)
if(yank == someOther) {
return otherObjectInjected
.forBreech.nextStep.Beep(y, z).okay(x => x.needed)
} else {
def bx = Switcher(get(e.error))
}
}
}
}
}
...
if (xxx === yyy) {
test.burn.findAll {
if(it....) {
....
}
}
}
}
}
まあ上記をクソース(クソなソースと書いてクソース)としよう。そして、おそらくあなたがCTOとかVPとかにならん限り、基本的にあなたが目にするのはクソースだし、世の中で動いている(そして企業が作っている)ほぼ9割のソースがクソースだと、断言しても過言ではない。
なぜならよいソースコードというのはBiz側にとっては何の価値も(悲しきかな)ないものであり、ビジネス要件がすべて、納期がすべてだから。そしてそれを我々は受け入れねばならない。(いやならハッカーよりのスタートアップにいくしかない)
問題は、このソースコードを解釈するコンパイラ(ないしVM)が「うんち」なケースがあり、エラーのスタックトレースを読んでも何が何だか直感的にわからないときだ。
SomeError: this value (e) is resilient value
com.middleware.Something 123: ...
com.middleware.Oxxx<$static.class> 22: ...
com.middleware.ElseController 55: ...
com.hibernate.some.thing 223: ...
com.middleware.PPP 555: ...
....
もちろん、多くの場合はスタックトレースを丁寧に読んでいけば解決するが、言語やフレームワークによってはまったくちんぷんかんだったり、stack overflow先生も「は?」で終わることがある。
さてどうするか。
ここで必要なのがにじり戦法である。
まず、バグを発症させない可能な限りの(推定)該当箇所をコメントアウトする
class Sample {
public someMethod(SomeStruct arg1, OtherStruct arg2, ElseStruct arg3) {
// def arg1Copy = copyClass.copySomeObject(arg1)
// List<Some> arg2Copy = ...
// if(arg3.nasty == arg2Copy.size()) {
// for(x in xs) {
// for (y in x) {
// object.domain.class(x, y, otherStruct).subscribe { a =>
// List event = a.eventGet(otherCode, arg4, arg5, arg6)
// if(yank == someOther) {
// return otherObjectInjected
// .forBreech.nextStep.Beep(y, z).okay(x => x.needed)
// } else {
// def bx = Switcher(get(e.error))
// }
// }
// }
// }
// }
// ...
// if (xxx === yyy) {
// test.burn.findAll {
// if(it....) {
// ....
// }
// }
// }
}
}
何らかの事情でコメントアウトが通用しない場合は throw Exceptionして止めるというてもある。とりあえず9割コメントアウトして走らせて、該当のエラーが再現するかを確認する。
class Sample {
public someMethod(SomeStruct arg1, OtherStruct arg2, ElseStruct arg3) {
def arg1Copy = copyClass.copySomeObject(arg1)
// List<Some> arg2Copy = ...
// if(arg3.nasty == arg2Copy.size()) {
// for(x in xs) {
// for (y in x) {
// object.domain.class(x, y, otherStruct).subscribe { a =>
// List event = a.eventGet(otherCode, arg4, arg5, arg6)
// if(yank == someOther) {
// return otherObjectInjected
// .forBreech.nextStep.Beep(y, z).okay(x => x.needed)
// } else {
// def bx = Switcher(get(e.error))
// }
// }
// }
// }
// }
// ...
// if (xxx === yyy) {
// test.burn.findAll {
// if(it....) {
// ....
// }
// }
// }
}
}
さて、1行コメントアウトを解除して、また走らせる。
その次も、またその次も、1行づつコメントアウトを解除して、辛抱強く走らせる。にじるように一個一個コメントアウトしては走らせるので、「にじり戦法」と呼ぶ。
もちろん、if節とかfor節とかクローングしているところは、 if節とそのお尻かっこをまずコメントアウト解除する
class Sample {
public someMethod(SomeStruct arg1, OtherStruct arg2, ElseStruct arg3) {
def arg1Copy = copyClass.copySomeObject(arg1)
List<Some> arg2Copy = ...
if(arg3.nasty == arg2Copy.size()) {
// for(x in xs) {
// for (y in x) {
// object.domain.class(x, y, otherStruct).subscribe { a =>
// List event = a.eventGet(otherCode, arg4, arg5, arg6)
// if(yank == someOther) {
// return otherObjectInjected
// .forBreech.nextStep.Beep(y, z).okay(x => x.needed)
// } else {
// def bx = Switcher(get(e.error))
// }
// }
// }
// }
}
// ...
// if (xxx === yyy) {
// test.burn.findAll {
// if(it....) {
// ....
// }
// }
// }
}
}
とまあこんな感じである。これをやると、どこかでエラーが「でる」「でない」の臨界点が特定できるので、あとはその臨界点を解剖してあげれば問題の特定が可能になる。
とまあ、こんなかんじでござる。
また、にじり戦法と 二分法(bisection method)を組み合わせてコメントアウトして特定していく方法もある。これは二分にじり戦法という。
以上である。
まとめ
エンジニアの良し悪しを分けるのは、1. 数学的なかんがえかた(そしてアルゴリズム)2. 経験値 3. ふだんどれだけ技術を勉強してるか 4. こころがけ の4つである。こころがけの例としては、「にじり戦法」というのがあり、バグの推定されるコードエリアを全て(エラーのでない限り)コメントアウトして、コメントアウトを1行ずつ外してはプログラムを走らせる、を繰り返しエラーの該当箇所を特定する。コンパイラなどがまともに正確なエラーを提示しない場合、中の状況が五里霧中な場合、これは多くの場面で有効なデバッグ方法である。
また次の記事でお会いしましょう!