ある程度大きなプログラムを作っているとき, 複数の同じような処理を別々の場所に記述したい場合がよくある. また,たくさんのプログラムを作るようになると, 他のプログラムで使ったのと同じような処理が再度必要になる場合もよくある. こんな場合に,効率よくプログラミングするには,どうすれば良いか?
単純肉体労働で済んでしまうが,時間と労力の無駄だ. 後々の修正作業も大変になり,品質も停滞してしまうだろう. 羅列は非効率!
作成だけでなく修正も楽になるので, 愚直(真面目)と同程度以下の低コストでありながら, 高品質化が可能になるだろう. もちろん頭脳労働は必要になるが, 効率的.
...作業に移る前に,いつもの準備をお忘れなく,
まず,非効率なソースコードの記述例を挙げておく.
ソースファイル polygons-ng.c:
#include "kame3d.h" int main() { int i; Init("Kame3D Polygons"); Goto(-3.0, -2.5); SetColor(1); for (i = 0; i < 3; i++) { // 三角形(triangle) Move(2.5); Turn(120.0); } Goto( 1.0, -2.5); SetColor(2); for (i = 0; i < 4; i++) { // 四角形(square) Move(2.5); Turn(90.0); } Goto( 1.0, 1.0); SetColor(3); for (i = 0; i < 5; i++) { // 五角形(pentagon) Move(2.0); Turn(72.0); } Goto(-3.0, 1.0); SetColor(4); for (i = 0; i < 6; i++) { // 六角形(hexagon) Move(2.0); Turn(60.0); } Goto(0.0, 0.0); Play(); return (0); }
複数の異なる多角形を描いているのだが, ソースコード的には, ほとんど同じ処理(for ループ)を何度も書いている.
これらは,「まったく同じ処理」ではないが, 「同じパターンの処理」である. 「ちょっと違う部分」を含んではいるが, ほとんど同じだ.
かつて「 違いがわかる男 」はプロの代名詞であったが, 実は,本当のプロなら「違い(ちがい,相違点)」だけでなく, 「類い(たぐい,共通点)」もわからなければならない.C言語では,次のように, 同じパターンの処理を関数として, ひとまとめに記述できる.
上のコードを元にして, 次のソースファイル polygons-func.c を作成しよう:
#include "kame3d.h" // 以下のコードの色分けについて:違い要素,類い要素 // 多角形を描く関数(n:頂点の個数,d:辺の長さ) void Polygon(int n, double d) { int i; for (i = 0; i < n; i++) { Move(d); Turn(360.0/n); } } // メイン関数 int main() { Init("Kame3D Polygons"); Goto(-3.0, -2.5); SetColor(1); Polygon(3, 2.5); // 三角形 Goto( 1.0, -2.5); SetColor(2); Polygon(4, 2.5); // 四角形 Goto( 1.0, 1.0); SetColor(3); Polygon(5, 2.0); // 五角形 Goto(-3.0, 1.0); SetColor(4); Polygon(6, 2.0); // 六角形 Goto(0.0, 0.0); Play(); return (0); }
ここで,用語を説明しておく:
関数化前後でプログラムの実行結果については,まったく変わらない. しかし,関数化によって,ソースコード全体が短かくなった. (この例では,実際には,ほとんど変わっていないが... より大規模なプログラムの場合, その可能性については想像できるハズだ.) 特に,main 関数がコンパクトになり, プログラム全体の見通しが良くなった.
優秀な諸君のことなので,次のような疑問を持つかもしれない. 「Goto(...) や SetColor(...) についても, Polygon 関数の定義に入れてしまえば良いのでは? その方が短かくなるし.」 確かに,その通りである.
しかし,このプログラムの場合では, それは「やりすぎ」かもしれない. 例えば,引数が多くなり,関数呼び出しの記述方法が憶えづらくなる. それに,多角形を連続的に描画したい場合や色を変えたくない場合には, かえって面倒なことになる. 「過ぎたるは及ばざるがごとし.」
結局,どの範囲までを関数化すれば良いかを考えるとき, 「唯一の正解」というものは存在しない. 「さまざまな解」があり,どの方法を選ぶかによって, プログラマのセンスが問われることになる. プログラムの設計は,エンジニアリングであると同時に, アートやサービスでもある. ユーザ(顧客)や仲間(同僚)や自分(三日後は他人同然)への 「おもてなしの心」が重要.
呼び出した側から呼び出された側へ引き渡されるデータが引数である. その逆に,呼び出された側から呼び出した側へデータを返すこともできる. それが戻り値である.
前回の in-circle.c を元に改造して, 戻り値ありの関数の定義例を示す:
...
// 中心からの距離の自乗
double Dist2()
{
double x, y;
x = PosX();
y = PosY();
return (x*x + y*y);
}
int main()
{
...
for (i = 0; i < 100; i++) {
if (Dist2() > r*r) { // if (x*x + y*y > r*r) と同じ
...
} else {
...
}
...
}
...
}
return (...) の括弧内の計算結果が戻り値であり, double Dist2() の double が戻り値の型(関数の型)である.
なお,この例では,改造前と比べて,ソースコード全体が長くなってしまっている. しかし,その代わり,メイン関数が短くなったので, このプログラムの処理内容についての全体的な見通しは良くなったハズだ.
通常のプログラムでは,規則的・固定的な動作しかしないが, 乱数を利用すれば,偶発的な変化を生み出せるようになる. kame3d ライブラリでは,乱数関数 int Rand(int n) が定義されている. この関数を呼び出すと,0 以上 n 未満の整数が返される.
Rand() の使用例:(in-circle.c の改造)
Turn(Rand(3600)/10.0); // 0.1(deg) 刻みのランダムな角度で回転
...
サイコロで場合分けの例:
n = Rand(3); // 1/3 の確率で場合分け
if (n == 0) {
...
} else if (n == 1) {
...
} else {
...
}
...
なお,この例では,Rand(3600)/10 としてはダメだ. 実数 10.0 で割ること. C言語では整数同士の計算結果は整数になるので, Rand(3600)/10 では, Rand(360) と同じこと(0.1 刻みではなく 1 刻みの角度) を無駄に長く書いたことになってしまう.
もちろん,単純に,様々な多角形を描きたいだけなら:(polygons-func.c の再改造)
for (...) { ... Polygon(Rand(...)..., ...); ... }
このように,if による場合分けは不要.
様々な図形を様々な大きさで描きたい場合, 次のようなコードが思いつくだろう:
void Triangle(double size) { ... } void Square(double size) { ... } void Star(double size) { ... } int main() { int type; double size; ... Goto(Rand(100)*0.1 - 5.0, Rand(100)*0.1 - 5.0); type = Rand(3); size = Rand(10)*0.1; if (type == 0) { Triangle(size); } else if (type == 1) { Square(size); } else if (type == 2) { Star(size); } ... }
しかし,これだと if 文の単調な羅列があり,非効率だ. 次のように,関数へのポインタの配列を使うと, 効率良く記述できる:
void Triangle(double size) { ... } void Square(double size) { ... } void Star(double size) { ... } int main() { void (*shape[])(double size) = { Triangle, Square, Star }; // ↑ 相違部分(関数名)だけを羅列し,配列に登録しておく. // shape[0](size) で Triangle(size), // shape[1](size) で Square(size), // shape[2](size) で Star(size) を呼び出せるようにした. ... // ↓ 実際に,どれかの関数を呼び出す. shape[Rand(3)](Rand(10)*0.1); ... }
ただし,この技を使う場合, 対象とする関数のプロトタイプ(関数の戻り値や引数の個数・型)を 統一しておく必要がある.
関数の定義と呼び出しを効果的に利用して, 複雑な図案を効率良く描くような タートルグラフィックスプログラムを自由に作成せよ.
担当教員へレポートを送信せよ: