入念に工夫を凝らしたパラメーター処理アプローチ、第二弾
先日、不思議なことが起こりました。私は新たなプログラムを書いていたのですが、もちろん、優秀なプログラマーよろしく、車輪の再発明のような真似をすることなく、利用できるものは利用するようにしていました。そのとき呼び出していたのは、必要となる値を計算してくれるユーティリティ プログラムでした。ところが、そのユーティリティ プログラムは、それまでずっと問題なく動作していたのですが、無効なデータを返してくるようになってしまいました。長年、問題なく動作できていたプログラムが突然おかしくなってしまうというのは、どういうことなのでしょうか。
この問いに対する答えとしては、ファンタジー作家の リック・クック氏の著作 の中に秀逸な一節があります。「今日のプログラミングは、より大きく性能の良い、間抜けな利用者でも簡単に扱えるプログラムを作ろうと努力するソフトウェア エンジニアと、より多くの善良で間抜けな利用者を産み出そうとする全人類との競走だ。今のところ、全人類が勝っている」 この場合、私は間抜けな利用者の側だったようです。この問題については、後ほどじっくり見て行こうと思います。
しかし、まずは、この問題を考えてみてください。次のような2列2行の表があります。
First | Second |
1 | 4 |
2 | 6 |
次のステートメントを実行した後で、表の値はどのようになるでしょうか。
update ThisTable
set First = Second / 2,
Second = First * 2
この問題の答えについては、後で説明することとします。では、当面のテーマに取り掛かりましょう。すなわち、プログラムでのパラメーターの使用法についてです。
パラメーターを分類する方法には、少なくとも2通りの方法があります。まずは1つの分類法です。
- 入力: 入力時のパラメーターの値は重要であり、その値を変更することはありません。
- 出力: 入力時のパラメーターの値は重要ではなく、その値を変更することがあります。
- 入出力: 入力点のパラメーターの値は重要であり、その値を変更することも、変更しないこともあります。
もうひとつの分類法です。
- 必須: 呼び出し元がこのパラメーターに値を渡さなかった場合、ハード(ターミナル)エラーになります。
- オプション(任意指定): 呼び出し元がこのパラメーターに値を渡さない場合、このパラメーターは読み取られないか、または変更されません。入力パラメーターまたは入出力パラメーターである場合は、デフォルト値を指定します。
12月の記事に記したように、何年か前に、私は 安心安全にパラメーターを処理する方法を考案しました。先に進む前に、その記事のご一読をお勧めします。
もっとも、その記事の要点としては、プログラムにはそれぞれ、以下の処理を行う初期化ルーチンを備えるべきだということです。
- すべての必須パラメーターがプログラムに渡されていることを確認する
- パラメーターの作業用コピーを作成する
- 渡されなかったオプション パラメーターの作業用コピーへデフォルト値をロードする
- 可能であれば、それぞれのパラメーターの値を検証する
- 検出されたパラメーター違反ごとに、呼び出し元に診断メッセージを送信する
- 1つでもパラメーター エラーが検出された場合は、呼び出し元にエスケープ メッセージを送信する
これらの他に、きちんと述べていなかった大事なことが2つありました。
- 必須の入力パラメーターの作業用コピーを作成する必要はない(作成しても構わない)。
- 入出力パラメーターには、2つの値、すなわち、パラメーター自体からまたはデフォルト値からの元の(変更されていない)値と、作業用コピーの現行値がある。
その記事では、3つの入力パラメーター(1つは必須、2つはオプション)を使用するプログラムの例を示しました。入出力パラメーターと出力パラメーターについても触れましたが、それらについての説明はしていませんでした。この記事では、その補足ができればと思います。
ある企業のセールス プロモーションの表があるとします。
列 | データ タイプ | コメント |
---|---|---|
ID | 文字(8) | 主キー |
StartDate | パック10進数(7,0) | CYYMMDD形式 |
EndDate | パック10進数(7,0) | CYYMMDD形式 |
以下がそのデータです。
ID | STARTDATE | ENDDATE |
---|---|---|
NEWYEAR | 1210101 | 1210114 |
TAKE5 | 1210101 | 1210128 |
JP | 1210111 | 1210116 |
以下は、セールス プロモーションのデータを取得するために、他のプログラムが呼び出すことができるプログラムです。入力パラメーターは、セールス プロモーションのIDのみであり、このパラメーターについてのデータが返されることになります。他のパラメーターは出力専用です。開始日(StartDate)および終了日(EndDate)は日付データ タイプで返され、キャンペーンの期間(Duration)は日数で計算されます。要求されたセールス プロモーションが表にない場合、プログラムは期間として0を返します。
**free
ctl-opt actgrp(*caller);
dcl-pi VAS0340R;
inPromotion char (8) const;
ouStartDate date;
ouEndDate date;
ouDuration packed (3);
end-pi;
dcl-f Promotions disk usage(*input) keyed;
chain inPromotion Promotion;
if %found();
ouStartDate = %date(StartDate: *cymd);
ouEndDate = %date(EndDate: *cymd);
ouDuration = %diff(ouEndDate: ouStartDate: *days) + 1;
else;
clear ouDuration;
endif;
return;
これが、最初の段落で述べていたようなルーチン、すなわち、長い間、問題なく動作していて突然おかしくなるタイプのコードです。
以下は、説明だけのための、必要最低限の機能のみの呼び出し側プログラムです。
**free
ctl-opt actgrp(*new);
dcl-s StartDate date;
dcl-s EndDate date;
dcl-s Days packed (3);
dcl-s PromotionID char (8);
dcl-pr GETPROMOT extpgm;
inPromotion char (8) const;
ouStartDate date;
ouEndDate date;
ouDuration packed (3);
end-pr GETPROMOT;
*inlr = *on;
PromotionID = 'TAKE5';
GETPROMOT (PromotionID: StartDate: EndDate: Days);
return;
StartDate、EndDate、およびDaysの値は、それぞれ2021-01-01、2021-01-28、および28になる、と言われたら、おそらく納得されるのではないでしょうか。
では、以下の呼び出し側プログラムについてはどうでしょう。戻り値がどのようになるか考えてみてください。
**free
ctl-opt actgrp(*new);
dcl-s Dummy date;
dcl-s Days packed (3);
dcl-s PromotionID char (8);
dcl-pr GETPROMOT extpgm;
inPromotion char (8) const;
ouStartDate date;
ouEndDate date;
ouDuration packed (3);
end-pr GETPROMOT;
*inlr = *on;
PromotionID = 'TAKE5';
GETPROMOT (PromotionID: Dummy: Dummy: Days);
return;
>
Dummyが2021-01-28で、Daysが1だと言い当てた方は、ご自分に拍手喝采を送ってあげてください。これがまさに、最初の段落で言おうとしていた問題です。関心があったのは期間のみであり、開始日および終了日ではなかったため、両方の日付フィールドに同じダミー変数を渡していました。
プログラマーが使用していたのがコピーではなく出力パラメーターだったため、期間は正しく計算されませんでした。
ouDuration = %diff(ouEndDate: ouStartDate: *days) + 1;
以下は、出力変数の作業用コピーを使用した、同じ呼び出される側のプログラムです。
**free
ctl-opt actgrp(*caller);
dcl-pi GETPROMOT;
inPromotion char (8) const;
ouStartDate date;
ouEndDate date;
ouDuration packed (3);
end-pi;
dcl-f Promotions disk usage(*input) keyed;
dcl-s wrkStart like(ouStartDate);
dcl-s wrkEnd like(ouEndDate);
dcl-s wrkDuration like(ouDuration);
chain inPromotion Promotion;
if %found();
wrkStart = %date(StartDate: *cymd);
wrkEnd = %date(EndDate: *cymd);
wrkDuration = %diff(wrkEnd: wrkStart: *days) + 1;
endif;
ouStartDate = wrkStart;
ouEndDate = wrkEnd;
ouDuration = wrkDuration;
return;
このバージョンでは、それぞれの出力パラメーター用に作業変数があります。すべての計算が作業変数を使用して行われます。出力パラメーターは、呼び出し元への復帰の直前まで参照されません。1つ目のパラメーターには作業変数を定義しませんでした。このバージョンは、例に示した2つの呼び出し側プログラムの両方で適切に動作します。もうひとつ、難問を抱えていました。
先に出題したSQLの問題を思い出してください。以下がその答えです。
First | Second |
2 | 2 |
3 | 4 |
皆さんの答えはどうだったでしょうか。IBMがUPDATEステートメントでよく似た手法を取っていることに気付かれたでしょうか。FIRSTおよびSECONDの値は、言うまでもなく、ある種のバッファーに保存され、直接は処理されません。私はどうして分かったのでしょうか。それは、SECONDを変更する計算では、変更されたFIRSTの値を使用しないからであり、また、列の入力値はバッファーに保存されると、数年前にKent Milligan氏から教わっていたからでもあります。列の新たな値は、表へのリライトまでロードされません。
パラメーターに直接アクセスする代わりに、作業変数を定義して使用するのは大変そうに思われるかもしれませんが、私はそうは思いません。安心安全であるというのは、大いに価値があります。