【入門編】ZERODIVIDE条件の発生原因とONユニット制御 – PL/Iの基本構文とデータ制御実践ガイド

皆さん、こんにちは!メインフレームの世界へようこそ!

JavaやCOBOLの経験をお持ちの皆さんにとって、PL/Iという言語は耳慣れないかもしれませんね。でも大丈夫。プログラミングの基本はどの言語も同じ。一つずつ紐解いていけば、PL/Iもきっと皆さんの強力な武器になりますよ。

今回は、PL/Iでプログラムを書いていると、あるいは既存のバッチプログラムを解析していると「あ、これか!」となるであろう、`ZERODIVIDE`条件について、その発生原因から華麗なハンドリング方法まで、とことん深掘りしていきましょう。

PL/Iプログラムって、そもそもどんな形をしているの?(超ミニマム解説)

まず、PL/Iプログラムの全体像をざっくりと理解しておきましょう。Javaの「クラス」やCOBOLの「プログラム」に相当するものが、PL/Iでは少し独特な顔をしています。

SAMPLE_PROGRAM: PACKAGE OPTIONS(MAIN);
//
/ これはPL/Iプログラムの全体を包むパッケージです。 /
/ Javaでいうと「クラス」のようなものだと思ってください。/
/ OPTIONS(MAIN)は、このパッケージが実行の起点となる /
/ メインプログラムであることを示します。 /
//

DCL SYSPRINT FILE STREAM OUTPUT;
/ SYSPRINTは標準出力ファイルです。printfやDISPLAYみたいなもの。 /

//
/ ここからが実際の処理を記述するプロシージャーです。 /
/ PACKAGE内には、複数のPROCEDUREを定義できます。 /
/ これもJavaのメソッドやCOBOLのセクション・プロシージャー /
/ に似ていますね。 /
//
MAIN_PROC: PROCEDURE OPTIONS(MAIN);
PUT SKIP LIST(‘Hello, PL/I World!’);
RETURN;
END MAIN_PROC;

END SAMPLE_PROGRAM;

見ての通り、`PACKAGE`の中に`PROCEDURE`があり、その`PROCEDURE`にも`OPTIONS(MAIN)`が付いています。少しややこしいですが、最初は「`PACKAGE OPTIONS(MAIN)`がプログラム全体を定義し、その中の`PROCEDURE OPTIONS(MAIN)`が実際の処理の入り口なんだな」くらいの認識でOKです。

さて、PL/Iの基本構造に触れたところで、本日の主役である`ZERODIVIDE`条件に話を移しましょう。

ZERODIVIDEって何? 「ゼロ除算」の恐怖!

`ZERODIVIDE`。名前からして想像がつくと思いますが、これは「ゼロで割ろうとしたときに発生する条件(エラー)」のことです。

「え、そんなのJavaでもCOBOLでも当たり前じゃない?」
そう思われた方もいるでしょう。その通り、どんな言語でもゼロ除算はご法度です。しかしPL/I、特にメインフレームの世界では、この`ZERODIVIDE`がちょっと特別な意味を持ってくるんです。

PL/Iの数値データ型、少しだけ知っておこう

PL/Iは、数値の扱いが非常に厳密です。特に基幹業務でよく使われるのが、`FIXED DECIMAL`という型です。

  • `DCL 変数名 FIXED DECIMAL(p, q);`
  • `p` は数値全体の桁数(精度)
  • `q` は小数点以下の桁数(スケール)

例えば、`DCL AMOUNT FIXED DECIMAL(7, 2);` と宣言すると、「小数点以下2桁を含む、合計7桁の符号付き10進数」を意味します。つまり、`±99999.99` のような数値を扱います。

この`FIXED DECIMAL`、COBOLの`PIC 9(5)V99` とか `USAGE PACKED-DECIMAL` に非常に近い感覚です。お金の計算など、誤差が許されない業務では非常に重要な型なんですよ。

なぜPL/IでZERODIVIDEが問題になりやすいのか?

他の言語では、例えば浮動小数点数(`FLOAT`)同士の除算でゼロ除算が発生しても、結果が`Infinity`になったり、特定の例外がスローされたりしますよね。

PL/Iの場合、特に`FIXED DECIMAL`や`FIXED BINARY`(固定小数点2進数)のような固定小数点演算でゼロ除算が発生すると、そのデフォルトの挙動は多くの場合、プログラムの異常終了です。これは基幹システムでは絶対に避けたい事態ですよね。

しかも、入力データが明示的にゼロでなくても、途中の計算結果がたまたまゼロになって、それが除数に使われる…なんてシナリオも珍しくありません。そして、PL/Iのコンパイラは非常に賢いので、最適化の過程で「あれ、ここでゼロ除算が起きる可能性があるぞ」と判断すると、事前に警告を出してくれることもあります。それでも実行時にしか分からないケースも多いので、注意が必要です。

ZERODIVIDEの発生原因をコードで見てみよう

それでは、具体的にどのようなコードで`ZERODIVIDE`が発生するのか、シンプルな例で見てみましょう。

ZERODIVIDE_SAMPLE: PACKAGE OPTIONS(MAIN);

DCL SYSPRINT FILE STREAM OUTPUT;

MAIN_PROC: PROCEDURE OPTIONS(MAIN);
DCL (NUMERATOR, DENOMINATOR) FIXED DECIMAL(7, 2) INIT(0);
DCL RESULT FIXED DECIMAL(10, 2);

PUT SKIP LIST(‘— ZERODIVIDE発生の例 —‘);

//
/ 例1: 除数が直接ゼロの場合 /
//
NUMERATOR = 100.00;
DENOMINATOR = 0.00; / 明示的にゼロをセット /

PUT SKIP LIST(‘分子: ‘, NUMERATOR, ‘ 除数: ‘, DENOMINATOR);
/ ここで ZERODIVIDE 条件が発生する /
RESULT = NUMERATOR / DENOMINATOR;
PUT SKIP LIST(‘結果: ‘, RESULT); / ここには到達しない /

PUT SKIP LIST(‘プログラムは異常終了します。’);
RETURN;
END MAIN_PROC;

END ZERODIVIDE_SAMPLE;

このコードを実行すると、`RESULT = NUMERATOR / DENOMINATOR;` の行で`ZERODIVIDE`条件が発生し、プログラムは多くの場合、ランタイムエラー(例えば`U4000`など、システムによって異なる)で異常終了してしまいます。

現場では、この`DENOMINATOR`がDBから取得した値だったり、別の計算結果だったりして、意図せずゼロになるケースがほとんどです。

ONユニット制御でZERODIVIDEを華麗にハンドリング!

さて、プログラムが異常終了してしまうのは困りますよね。そこでPL/Iが提供してくれる強力な機能が `ONユニット` です。`ONユニット`は、特定の条件(例外)が発生したときに、どんな処理を実行するかを事前に定義しておく仕組みです。Javaの`try-catch`ブロックやCOBOLの`ON SIZE ERROR`句に似ていますが、より柔軟で強力だと感じていただけるはずです。

`ZERODIVIDE`条件をハンドリングするには、`ON ZERODIVIDE`を使います。

ZERODIVIDE_HANDLER: PACKAGE OPTIONS(MAIN);

DCL SYSPRINT FILE STREAM OUTPUT;

MAIN_PROC: PROCEDURE OPTIONS(MAIN);
DCL (NUMERATOR, DENOMINATOR) FIXED DECIMAL(7, 2);
DCL RESULT FIXED DECIMAL(10, 2);
DCL ERR_FLG BIT(1) STATIC INIT(‘0’B); / エラーフラグ /

PUT SKIP LIST(‘— ZERODIVIDE ONユニットの例 —‘);

//
/ ON ZERODIVIDE 文で ZERODIVIDE 条件発生時の処理を定義 /
/ このブロックは、ZERODIVIDE が発生したときに実行されます /
/ Javaのcatchブロックのようなものですね。 /
//
ON ZERODIVIDE BEGIN;
PUT SKIP LIST(‘

ZERODIVIDE 条件が発生しました! #’);

PUT SKIP LIST(‘分子: ‘, NUMERATOR, ‘ 除数: ‘, DENOMINATOR);
PUT SKIP LIST(‘この除算は実行できません。結果は0とします。’);
RESULT = 0; / 結果を0に設定する /
ERR_FLG = ‘1’B; / エラーフラグを立てる /
/ ONユニットの処理が終わると、ZERODIVIDEが発生した演算の次行から処理が再開されます。/
END;

//
/ 例1: 除数が直接ゼロの場合 /
//
NUMERATOR = 100.00;
DENOMINATOR = 0.00;

PUT SKIP LIST(‘試行1: 分子: ‘, NUMERATOR, ‘ 除数: ‘, DENOMINATOR);
/ ここで ZERODIVIDE 条件が発生し、ONユニットが実行される /
RESULT = NUMERATOR / DENOMINATOR;
PUT SKIP LIST(‘試行1: 結果: ‘, RESULT, ‘ (ERR_FLG: ‘, ERR_FLG, ‘)’);

IF ERR_FLG THEN DO;
PUT SKIP LIST(‘試行1: エラー処理を続行します。’);
ERR_FLG = ‘0’B; / フラグをリセット /
END;

//
/ 例2: 除数がゼロではない場合(正常系) /
//
NUMERATOR = 200.00;
DENOMINATOR = 5.00;

PUT SKIP LIST(‘試行2: 分子: ‘, NUMERATOR, ‘ 除数: ‘, DENOMINATOR);
/ ZERODIVIDE は発生しないので、ONユニットは実行されない /
RESULT = NUMERATOR / DENOMINATOR;
PUT SKIP LIST(‘試行2: 結果: ‘, RESULT, ‘ (ERR_FLG: ‘, ERR_FLG, ‘)’);

PUT SKIP LIST(‘— プログラムは正常終了しました。 —‘);
RETURN;
END MAIN_PROC;

END ZERODIVIDE_HANDLER;

このコードを実行すると、`ZERODIVIDE`が発生してもプログラムは異常終了せず、`ON ZERODIVIDE`ブロックで定義した処理が実行され、その後、問題の除算の次の行から処理が継続されます。

ONユニットでできること

`ONユニット`の中では、以下のような処理が可能です。

  • エラーメッセージの出力: `PUT SKIP LIST`などで、状況をログに出力します。
  • 代替値の設定: `RESULT = 0;` のように、演算結果に適切な値を代入して処理を継続させます。
  • エラーフラグの設定: `ERR_FLG = ‘1’B;` のようにフラグを立てて、後続の処理でエラーを判断できるようにします。
  • 別のプロシージャー呼び出し: `CALL ERROR_HANDLER_PROC;` のように、専用のエラー処理ルーチンを呼び出すこともできます。
  • `GOTO`によるジャンプ: 特定のラベルに処理を飛ばすことも可能ですが、これはコードの可読性を損ねる可能性があるため、あまり推奨されません。
  • `REVERT ZERODIVIDE;`: 現在有効な`ON ZERODIVIDE`を無効にし、その前の`ON ZERODIVIDE`定義(もしあれば)を有効に戻します。
  • `SIGNAL ZERODIVIDE;`: 意図的に`ZERODIVIDE`条件を発生させることができます。テストやデバッグで役立つことがあります。

`ONユニット`は、そのスコープ内で発生する条件に対して有効になります。例えば、あるプロシージャー内で`ON ZERODIVIDE`を定義すれば、そのプロシージャー内でのみ有効です。異なるプロシージャーで異なるエラーハンドリングを適用することも可能なんですよ。

ZERODIVIDEハンドリングのベストプラクティスと現場の知見

さて、`ON ZERODIVIDE`を使えば、とりあえずプログラムの異常終了は防げます。しかし、ただエラーを握りつぶすだけでは、後々大きな問題になる可能性があります。基幹システムでは特に、データの一貫性や整合性が命だからです。

1. エラーは必ずログに残す

「ゼロ除算が発生したけど、何とか処理は続いたよ!」だけでは不十分です。いつ、どこで、なぜ、どんな値でゼロ除算が発生したのかを、詳細にログに出力することが極めて重要です。これにより、後から原因を特定し、入力データの修正やプログラムの改善につなげることができます。

ON ZERODIVIDE BEGIN;
PUT FILE(SYSERR) SKIP LIST(
‘ERR-0010: ZERODIVIDE発生!’,
‘日時: ‘, DATETIME(),
‘プロシージャー: MAIN_PROC’, / 現在のプロシージャー名 /
‘分子: ‘, NUMERATOR,
‘除数: ‘, DENOMINATOR
);
RESULT = 0; / またはビジネスロジックに応じた代替値 /
ERR_FLG = ‘1’B;
END;

`SYSERR`はエラーログ用のファイルで、`DCL SYSERR FILE STREAM OUTPUT;`のように定義して使います。

2. ビジネスロジックに基づいた適切な代替値を設定する

`RESULT = 0;` とするのは最も簡単な回避策ですが、それが常に正しいとは限りません。

  • 例えば「平均値を求める計算で、要素数が0だった」場合、平均値を0とするのが正しいのか?
  • 「単価が0円の商品で個数を割ろうとした」場合、結果はどうあるべきか?

ビジネス要件に応じて、「結果は-1にする」「NULL値(PL/Iでは少し特殊ですが)として扱う」「このレコードは処理をスキップする」など、適切な判断を導く必要があります。

3. エラーフラグや状態管理を徹底する

`ERR_FLG`のようなフラグを使って、エラーが発生したことを上位の処理に伝えるのは非常に有効です。これにより、後続の処理でエラーフラグを見て、例えば「このレコードは異常だったので、後で手動で修正が必要なリストに加える」といった制御が可能になります。

4. テストは入念に!特に境界値とゼロ値

開発段階で、除数になり得る項目には「0」や「null(未設定)」、「非常に小さい値」などを意図的に入力し、`ZERODIVIDE`が発生しないか、発生した場合に適切にハンドリングされるかを徹底的にテストすることが肝心です。

5. 安易な`ZERODIVIDE`抑制は危険信号

現場で稀に見かけるのが、「とりあえず`ON ZERODIVIDE GOTO ABEND_LABEL;`でエラー処理に飛ばす」とか「`ON ZERODIVIDE SYSTEM;`(デフォルトのシステムアクションに戻す、つまり異常終了)にしてしまう」といったケースです。
あるいは、何も考えずに`ON ZERODIVIDE BEGIN; RESULT = 0; END;`としてしまうことも。

これらは、一時的な回避策としては有効かもしれませんが、根本原因を解決せず、単にエラーを隠蔽しているだけの場合が多いです。特に、異常終了を回避するために安易に結果を0にしてしまうと、その後の集計やレポートで誤った数値が算出され、データ不整合やビジネス上の誤判断につながる可能性があります。

`ZERODIVIDE`が発生するということは、何らかのデータ異常か、ロジックの考慮漏れがあるというサインです。そのサインを見逃さず、真摯に向き合うことが、堅牢な基幹システムを構築・維持する上で最も重要なことなんです。

まとめ:ZERODIVIDEはPL/Iの奥深さを知る入り口

いかがでしたでしょうか? `ZERODIVIDE`というシンプルな条件一つとっても、PL/Iが持つ堅牢性、柔軟性、そして基幹システムにおけるデータ処理の厳密さの一端が垣間見えたかと思います。

Javaの`try-catch`やCOBOLの`ON SIZE ERROR`に慣れている皆さんにとっては、PL/Iの`ONユニット`は最初は少し戸惑うかもしれません。しかし、その強力な例外ハンドリングの仕組みは、メインフレームの基幹システムが長年培ってきた信頼性と安定性を支える重要な要素の一つです。

恐れることはありません。一つ一つの条件、一つの構文には、必ず「なぜそうなっているのか」という背景と、それを使う上でのベストプラクティスが存在します。PL/Iの世界は奥深いですが、一つずつ紐解いていけば、きっとその魅力に気づいていただけるはずです。

これからも一緒に、メインフレームとPL/Iの知見を深めていきましょう!

タイトルとURLをコピーしました