以下はコード 11.1 の再掲です。 よく見てください。黄色い部分はほぼ同じフレーズだと思いませんか?
void Loop(void) { if (AvoidMode==1) { if (!(gSurTiles[5] & OBSTACLE_TILE)) NaviCmd(0,2); else if (gSurTiles[0] & OBSTACLE_TILE) NaviCmd(0,1); else { NaviCmd(1,0); if (isTgtAhead()) AvoidMode=0; } } else { if (gSurTiles[0] & OBSTACLE_TILE){ NaviCmd(0,1); AvoidMode=1; } else if ((gSurTiles[1] & NEXTFLAG_TILE) || (gSurTiles[8] & NEXTFLAG_TILE)) NaviCmd(0,1); else if (isTgtAhead()) NaviCmd(1,0); else NaviCmd(0,1); } } |
この部分は何をしているのでしょうか。 gSurTiles[] というのはロボット周囲の情報でした。
それに & OBSTACLE_TILE を添えれば、その六角形が 障害物かどうかを知ることができる約束でした。 つまり、gSurTiles[N] & OBSTACLE_TILE は N 番の六角形が障害物かどうかを知るフレーズだったのです。 決まりきったフレーズであり、約束事と言えばそれまでなのですが、 慣れないと分かりにくいですね。 そこで、もっと端的に何をしているのか分かるようにしましょう。 関数を使うのです。
関数には名前が必要です。 その六角形が障害物かどうかを知りたいわけですから、 「障害物か」をローマ字化した ShogaibutuKa でもかまいません。 コード 12.1 は次のようになります。
void Loop(void) { if (AvoidMode==1) { if (!ShogaibutuKa(5)) NaviCmd(0,2); else if (ShogaibutuKa(0)) NaviCmd(0,1); else { NaviCmd(1,0); if (isTgtAhead()) AvoidMode=0; } } else { if (ShogaibutuKa(0)){ : 〜 中略 〜 } } |
関数の名前は重複しなければなんでもよく このようにローマ字にしても構わないのですが、 執筆者の精神にダメージを与えます。 私の精神の安定を保つため、以降は isObstacle にさせてください. obstacle は障害物のこと、それに be 動詞の is を頭につけて疑問形にしたのです。
void Loop(void) { if (AvoidMode==1) { if (!isObstacle(5)) NaviCmd(0,2); else if (isObstacle(0)) NaviCmd(0,1); else { NaviCmd(1,0); if (isTgtAhead()) AvoidMode=0; } } else { if (isObstacle(0)){ : 〜 中略 〜 } } |
コード 12.3 では isObstacle() という関数を使うことで、プログラムの意図が分かり易くなるためです。 しかしそれは人間が "Is it obstacle?" とこの中でやろうとしている意図が分かるためです。 コンピュータにはそれができません。 そのため isObstacle() がそういう意味だと、もっと正確に言うとそういう内容を実現する処理だと きちんと伝えないといけません。 isObstacle() の本体を書くのです。
bool isObstacle(int n){ return gSurTiles[n] & OBSTACLE_TILE; } void Loop(void) { if (AvoidMode==1) { if (!isObstacle(5)) NaviCmd(0,2); else if (isObstacle(0)) NaviCmd(0,1); else { NaviCmd(1,0); if (isTgtAhead()) AvoidMode=0; } } else { if (isObstacle(0)){ NaviCmd(0,1); AvoidMode=1; } else if ((gSurTiles[1] & NEXTFLAG_TILE) || (gSurTiles[8] & NEXTFLAG_TILE)) NaviCmd(0,1); else if (isTgtAhead()) NaviCmd(1,0); else NaviCmd(0,1); } } |
コード 12.4 は isObstacle() の実体も含めたプログラムです。 実体は最初の三行です。 これが Loop() 関数の外に書かれていることに注意してください。 ここは三つの領域で言う所の赤いトップレベルです。 最初の一行は「こういう関数の中身 (実装) を今から記述します」という宣言です。 最後の左波括弧はここから中身が始まりますという合図なので、宣言部分は
部分です。 この宣言は全部で三つのパート (黄色と水色と桃色) に分かれています。 このうち水色の isObstacle は関数の名前です。 もしコード 12.2 のように ShogaibutuKa という名前の関数を使いたければこの部分を書き換えます。 次に桃色の int n ですが、これは関数に与えるパラメータの数と種類そして関数内での名前です。 詳しい説明は省きますが、とにかくこう書くと、isObstacle(5) や isObstacle(0) と呼び出されると n に 5 や 0 が入ります。 そして最後の 黄色 部分は関数の戻り値の型です。 ここでは、障害物があるかないか、成り立つか成り立たないかの二択なので bool という型を使ってますが、int でもかまいません (ただし Visual Studio が「書き間違いでは?」と警告することがあります)。
二行目は関数の実態です。ここでは return gSurTiles[n] & OBSTACLE_TILE; と一行だけ書いていますが、 Loop() 関数のように複数行書いてもかまいません。
コード 12.1 の gSurTiles[5] & OBSTACLE_TILE は「5 番目のタイルが障害物かどうか」を知る記述であり、 コード 12.4 では、これを関数化し isObstacle(5) と記述できるようにしました。 一方、コード 12.1 や コード 12.4 にある (gSurTiles[1] & NEXTFLAG_TILE) || (gSurTiles[8] & NEXTFLAG_TILE) は「右斜め前のタイルにゴールがあるか」を判定する処理です。 これも分かりにくいので isGoalAheadRight() という関数を作成しましょう。
bool isGoalAheadRight(void){ return (gSurTiles[1] & NEXTFLAG_TILE) || (gSurTiles[8] & NEXTFLAG_TILE); } bool isObstacle(int n){ return gSurTiles[n] & OBSTACLE_TILE; } void Loop(void) { if (AvoidMode==1) { if (!isObstacle(5)) NaviCmd(0,2); else if (isObstacle(0)) NaviCmd(0,1); else { NaviCmd(1,0); if (isTgtAhead()) AvoidMode=0; } } else { if (isObstacle(0)){ NaviCmd(0,1); AvoidMode=1; } else if (isGoalAheadRight()) NaviCmd(0,1); else if (isTgtAhead()) NaviCmd(1,0); else NaviCmd(0,1); } } |
こういったコードを読む時は頭から読んではいけません。 着目する機能を実現している関数から読み始めると理解しやすくなります。 このプログラムのメインは Loop() 関数で、 ここで各ステップの行動を決めます。 関数は「何をするものか」を意識しながら読めば大丈夫です。 すべての細かい動作を理解しようとするのではなく、まずは 「この関数は〇〇のためのもの」という全体像を捉えましょう。 そのような視点でコード 12.5 の Loop() 関数を見ると、 かなりすっきりして、何をさせようとしているのかが明確になります。
もっとシンプルにできます。 この Loop() の大本はコード 9.3 と 9.6で、 アルゴリズムを日本語化すると以下のようになります。
この方針に従ってコードを整理すると、以下のようになります。 今回作った AvoidModeStep() と NormalModeStep() という二つの関数はパラメータを必要としません。 そのため先述の桃色部分のパラメータは「無い」という意味で void にしています。 またこの関数は戻り値を使わない (「return なんとか」という文が無い) ので、 戻り値の型も void にしています。
bool isGoalAheadRight(void){ return (gSurTiles[1] & NEXTFLAG_TILE) || (gSurTiles[8] & NEXTFLAG_TILE); } bool isObstacle(int n){ return gSurTiles[n] & OBSTACLE_TILE; } void AvoidModeStep(void){ if (!isObstacle(5)) NaviCmd(0,2); else if (isObstacle(0)) NaviCmd(0,1); else { NaviCmd(1,0); if (isTgtAhead()) AvoidMode=0; } } void NormalModeStep(void){ if (isObstacle(0)){ NaviCmd(0,1); AvoidMode=1; } else if (isGoalAheadRight()) NaviCmd(0,1); else if (isTgtAhead()) NaviCmd(1,0); else NaviCmd(0,1); } void Loop(void) { if (AvoidMode==1) AvoidModeStep(); else NormalModeStep(); } |
Loop() を見るだけで、動作の流れが一目瞭然となりました。
このように処理を細かく分け、それぞれを関数という形で表現することで、 プログラムが読みやすくなるだけでなく、様々なメリットを生み出します。
一つ目のメリットはプログラムが作りやすくなることです。 プログラムを関数毎に分割すると、一度に全体を考える必要がなくなり、 それぞれの機能に集中して作ることができます。 例えばコード 12.5 の AvoidModeStep() には、次の役割が期待されています。
この関数を作る時はプログラム全体の動きを細かく把握する必要はありません。 AvoidModeStep() の目的だけ考えればよいため、 作業の負担が軽減されます。
特定の機能を差し替えられることも森っとです。 関数を独立した部品なのでそこを入れ替えればいいのです。 例えば、より高度な障害物回避アルゴリズムを考えた場合、 AvoidModeStep() の中身をそれに変えるだけで、 新しい動作をするロボットが完成します。 また A 君が作ったより高度な AvoidModeStep() を B 君のロボットに組み込むことで、 B 君のロボットも賢くなるというように、 関数の共有によって様々な組み合わせが可能になります。
知識や技術が継承できるのもメリットです。 関数は、必ずしも同期の友人が作る必要はありません。 先輩が作ったものを引き継ぎ、活用することも可能です。 例えば、昨年の先輩が時間をかけて素晴らしい AvoidModeStep() を作成していたとしましょう。 その場合、今年のチームは AvoidModeStep() をそのまま活用し、 NormalModeStep() の開発に注力するといった形で 効率よくプログラミングを進めることができます。
関数を的確に使うことで、デバッグを効率的に行えることもメリットです。 バグの特定と修正も容易になるからです。 例えば、 AvoidModeStep() 開発する際、回避動作に問題があると判明した場合、 修正すべき箇所は AvoidModeStep() 内に限定されます。 AvoidModeStep() の修正が不十分でまだバグが存在するかもしれませんが、 AvoidModeStep() 以外を修正しないことで、そこから起因する新たなバグの混入を防ぐことができます。 また、もし先輩が作った素晴らしい AvoidModeStep() が既に多くのロボット で使用され十分に動作確認されているのなら、 ここにバグが入っている可能性は低いと考えていいでしょう。 何かロボットが問題ある動きをしている場合、
一度作成した関数は、チームや組織の 作った関数は所属組織の資産となります。 できるだけ機能を関数で部品化し、それを組み合わせることでより高度なプログラムを作ってみてください。