サンプル・プログラム
条件には順序がある
一方、if~else文 の順序を変えた次のプログラムでは、西暦1900年もうるう年になってしまうというプログラムのバグが発生する。
このことから、条件には順序があることが分かる。
19: <script>
20: /**
21: * 判定と画面表示
22: */
23: function ifElse5() {
24: //変数宣言
25: let year = document.getElementById('year').value; //西暦年
26: year = parseInt(year, 10); //10進整数化
27: let ret;
28:
29: //うるう年判定(順序の間違い)
30: if (year % 4 == 0) {
31: ret = 'うるう年';
32: } else if (year % 100 == 0) {
33: ret = '平年';
34: } else if (year % 400 == 0) {
35: ret = 'うるう年';
36: } else {
37: ret = '平年';
38: }
39:
40: //結果を表示する
41: document.getElementById('ret').innerHTML = ret;
42: }
43: </script>
うるう年のケースでは、年を要素とするベン図を描いてみる。すると、左図のように入れ子になる。
19: <script>
20: //2次元配列:各配列の1番目は作物名,2番目以降は種類
21: let Produces = [
22: ["キャベツ", "野菜"],
23: ["キュウリ", "野菜"],
24: ["リンゴ", "果物"],
25: ["ミカン", "果物"],
26: ["トマト", "野菜", "果物"],
27: ];
28:
29: /**
30: * 指定した作物が指定した種類かどうかを判定する.
31: * produceに作物名を,categoryに種類を代入し,配列Producesと照合する.
32: * Producesに未登録の作物についてもfalseを返す.
33: */
34: function isCategoryProduce(produce, category) {
35: for (let item of Produces) {
36: //作物名が見つかった
37: if (item[0] == produce) {
38: for (let i = 1; i < item.length; i++) {
39: //種類が見つかった
40: if (item[i] == category) {
41: return true;
42: }
43: }
44: }
45: }
46: return false;
47: }
48:
49: /**
50: * 入力した作物の種類を判定し,画面に表示する.
51: */
52: function ifElse6() {
53: //作物名
54: let produce = document.getElementById('produce').value;
55:
56: if (isCategoryProduce(produce, "野菜") && isCategoryProduce(produce, "果物")) {
57: ret = "野菜かつ果物";
58: } else if (isCategoryProduce(produce, "野菜")) {
59: ret = "野菜";
60: } else if (isCategoryProduce(produce, "果物")) {
61: ret = "果物";
62: } else {
63: ret = "未登録";
64: }
65:
66: //判定結果を表示する
67: document.getElementById('ret').innerHTML = ret;
68: }
69: </script>
この場合も、部分集合が小さなもの(この場合は積集合)から大きなものへ向かって順に if~else文 を書いてゆく。"野菜" と "果物" の集合の大きさは不定なので、これらの順序はどちらが先でも構わない。未登録の作物は、最後に else として扱う。
条件の抜け漏れ
そこで条件の抜け漏れがないことを単体テストでチェックできるよう、独立した関数にしてしまう方法がある。
20: /**
21: * 指定した月が大かどうかを判定する.
22: * @param int month 月(1~12)
23: * @return bool true:大の月/false:小の月/null:monthが定義域外
24: */
25: function isLongMonth(month) {
26: //月の大小を配列にしておく
27: months = new Array();
28: months[1] = true;
29: months[2] = false;
30: months[3] = true;
31: months[4] = false;
32: months[5] = true;
33: months[6] = false;
34: months[7] = true;
35: months[8] = true;
36: months[9] = false;
37: months[10] = true;
38: months[11] = false;
39: months[12] = true;
40:
41: let ret = null;
42: //monthsの中で月が一致するものを探して判定する
43: for (let m = 1; m <= months.length; m++) {
44: if (m == month) {
45: ret = months[m];
46: break;
47: }
48: }
49: return ret;
50: }
さらに、各々の条件が独立している(和集合がない)ことから、isLongMonth の中では、1~12月の大小定義を配列 months に持たせておき、forループを使って指定した月 month と合致するものの結果を返すようにしている。
条件を配列にすることで全体の見通しが良くなり、抜け漏れを防止することができる。
原則:各々の条件が独立している場合は、配列を利用して見通しをよくする。
無限ループ
たとえば近似計算法の一種である「3.4 whileループ - ニュートン法」だが、例で採り上げた平方根の場合はリニアに近似解が求められるのでいいのだが、ものによっては近似解が振動するものがある。
JavaScriptではなくPHPプログラムになるが、「PHPで月齢を計算」で紹介した、次の朔の日時を求める関数がそれである。しかも、振動が収束するとは限らず、ループを繰り返すとかえって拡散するケースがある。
このように振動するケースでは、whileループ が無限ループに陥る可能性がある。しかも、かならず無限ループになるわけではないというのが厄介なところだ。
ここで、ループ制御を別の視点から見てみよう。
forループ にしても whileループにしても、カウンタ変数を置いて、それが一様に増えていく(または減っていく)ことを前提に制御しているものは安全にループから脱出できる。逆に、振動するようなカウンタ変数(上述のケースでは誤差変数)をループ制御の終了条件とするのは、リスクが高い。
そこで、次の朔の日時を求める関数では、別にカウンタ変数を置いて、for文で繰り返す最大回数を制限することで無限ループに陥ることを回避した。
得られる結果の誤差は大きいかもしれない。しかし、誤差が大きいことを利用者に提示できれば、無限ループに陥ってシステムダウンするよりはマシであろう。
原則:ループ制御のカウンタは振動しないものを選ぶ。
また、カウンタ変数を使わず、「読みやすいプログラム:ループ処理」で紹介した方法を適用してみるのも有効な手段だ。
原則:ループ処理では、for...in, forEachメソッド, mapメソッドの適用を優先する。
コラム:その制御は必要か?
だが、条件分岐やループ制御を書こうとすると、途端に面倒なことになる。これはどうしたことか――。
なぜ難解かといえば、実際の仕事や日常生活で制御を使うことはあまり多くないからだ。たとえば仕事上で何かを申請するときは、条件分岐がせいぜい2つか3つで、それに応じて申請書式が変わることが多い。つまり、条件制御するのではなく、入力画面を変えればいいのだ。
ループ制御に至っては、そんな業務があったら上司から省力化(リニア化)しろと言うに違いない。
だから、プログラム初心者は制御のイメージが頭に思い浮かばない。
ローコード/ノーコードは、利用者が利用者視線でツールを用意して業務の効率化・改善に繋げようという目論見から始まっているから、制御がなくても支障ないのである。
むしろ、われわれプログラマの方が、本当にその制御は必要なのか、省くことはできないのか――と考えた方が読みやすいプログラムに繋がる。本当に制御が必要な部分は独立させ、時間をかけて開発・テストを実施したうえで、DXツールとコネクトするといい。
そこで今回は、制御に潜むバグの代表例を取り上げ、こうしたバグを予防する方法を紹介する。