こんにちは!メインフレームの世界へようこそ。
JavaやCOBOLといったモダン、あるいは従来型のビジネス言語をバリバリ書いてきた方にとって、IBMメインフレームの「PL/I(ピーエルワン)」という名前を聞くだけで、なんだか古めかしくて難解な要塞をイメージしてしまうかもしれませんよね。
「なんだか記号だらけで怖そう……」
「変数のルールも独特なんでしょう?」
いえいえ、そんなことありませんよ。確かに歴史の長い言語ですが、一つひとつの仕様を紐解いていくと、実はプログラマの意図にすごく優しく寄り添って作られていることに気づくはずです。
今回は、そんなPL/Iの懐の深さがよくわかる「ゼロ除算(割り算の分母がゼロになっちゃった!)のトラップとリカバリ」について、実務の現場でおなじみのエピソードを交えながら、優しく丁寧にお話ししていきますね。
—
1. 他言語出身者が驚く? PL/Iの「予約語がない」世界
まず、PL/Iを学び始めて一番最初に「おっ?」と面食らうポイントが、識別子(変数名など)のルールです。
JavaやCOBOLには、「これはプログラミング言語の命令だから変数名に使っちゃダメ!」という「予約語(リワード)」がたくさんありますよね。例えば、COBOLで `DISPLAY` や `PAGE` なんていう名前の変数を定義しようものなら、コンパイラに怒られてしまいます。
ところが、PL/Iには厳密な意味での「予約語」が存在しません。
どういうことかと言うと、こんなコードが書けちゃいます。
1
/ こんな変数名だって許されちゃうのがPL/I /
DCL IF FIXED DEC(5,0);
DCL THEN FIXED DEC(5,0);
IF = 10;
THEN = 20;
「えっ、`IF` とか `THEN` って条件分岐のキーワードじゃなかったの!?」って思いますよね。
PL/Iのコンパイラは非常に賢く、前後の文脈(コンテキスト)を見て「あ、ここは変数だな」「ここは命令だな」と勝手に判断してくれます。そのため、変数名に制限が少なく、自由度が高いのが特徴です。
ただ、自由度が高いからといって `IF` や `THEN` を変数名にするのは、後からコードを読む人が気絶しかねないので絶対にやめましょうね(笑)。でも、この「プログラマを縛りすぎない」という設計思想、なんだか少し親近感が湧いてきませんか?
—
2. バッチ処理の悪夢「ゼロ除算」とどう向き合うか?
さて、本題に入りましょう。
基幹システムの夜間バッチ処理などで、大量の売上データを処理していると、たまにこんな悪魔のデータが流れてきます。
- 「販売数量:0個」なのに、「売上金額:10,000円」を割る計算式が入っている!
- マスターデータの登録ミスで、割る数がブランク(実質ゼロ)になっている!
Javaなら `ArithmeticException`、COBOLなら大小のABEND(異常終了)を引き起こし、夜中の3時にオペレータからあなたへ「システムが止まりました!」と悲痛な電話がかかってくる原因になります。
メインフレームの世界では、プログラムが派手にコケる(ABENDする)ことは、時として数百万円、数千万円の損失を意味します。「あー、ゼロで割っちゃったから即座に強制終了しますね」では、プロフェッショナルな基幹システムとは言えませんよね。
そこで登場するのが、PL/Iが誇る超強力な例外処理機能、`ONユニット(ON-condition)` です。
—
3. 実践! `ON ZERODIVIDE` で優しくリカバリするコード
「ゼロ除算が発生したら、プログラムを止めずに、とりあえず安全なデフォルト値(例えば「0」や「99999」)に置き換えて処理を続けたい!」
そんな願いを叶えるPL/Iのコードがこちらです。実務の改修でそのまま使えるよう、大文字ベースで丁寧にコメントを入れました。
1
ZERODIVIDE_SAMPLE: PROC OPTIONS(MAIN);
/ — 変数宣言エリア — /
/ FIXED DECIMAL はCOBOLのPACKED-DECIMAL(COMP-3)に似た、金融計算に強い固定小数点型です /
DCL TOTAL_SALES FIXED DEC(9,2) INIT(150000.00); / 総売上 /
DCL ITEM_COUNT FIXED DEC(5,0) INIT(0); / 販売数量(今回はゼロ!) /
DCL UNIT_PRICE FIXED DEC(9,2); / 単価(ここに計算結果が入る) /
/ — ゼロ除算トラップ(ONユニット)の定義 — /
/ 「もしゼロ除算が起きたら、慌てず騒がずこの処理を実行してね」と宣言します /
ON ZERODIVIDE
BEGIN;
DISPLAY(‘【警告】ゼロ除算を検出し代替値を設定します。’);
/ ゼロで割った場合のフェイルセーフとして「0」を強制代入 /
UNIT_PRICE = 0;
/ ※注意: ゴトゴト処理を続けるために、発生箇所の「次の行」へ処理を戻します /
END;
/ — メインの計算処理 — /
DISPLAY(‘処理を開始します…’);
/ ここで 150,000 ÷ 0 が実行されるため、通常なら即座にABENDします /
/ しかし、上記のONユニットが待ち構えているため、プログラムは止まりません! /
UNIT_PRICE = TOTAL_SALES / ITEM_COUNT;
/ — 結果出力 — /
DISPLAY(‘計算された単価は: ‘ || TRIM(CHAR(UNIT_PRICE)) || ‘ 円です。’);
DISPLAY(‘プログラムを正常に終了します。’);
END ZERODIVIDE_SAMPLE;
このコードの何が素晴らしいのか?
1. プログラムが死なない
`ITEM_COUNT` が `0` であるため、`TOTAL_SALES / ITEM_COUNT` の瞬間にハードウェア割り込み(例外)が発生します。しかし、OSやコンパイラがパニックを起こす前に、事前に仕掛けておいた `ON ZERODIVIDE` のブロックがフックし、華麗にキャッチしてくれます。
2. 安全なデフォルト値へのフォールバック
ONユニットの中で `UNIT_PRICE = 0;` と明示的に値を書き換えているため、後続の処理で「未初期化の値」や「ゴミデータ」をばら撒く心配がありません。
3. バッチ全体の耐障害性向上
1件の不正データのせいで、数百万件ある他の正常なデータまで処理が止まってしまう「ドミノ倒し事故」を完全に防ぐことができます。
—
4. スペシャリストからのワンポイント・アドバイス
ここで、レガシー移行や保守現場でありがちな「ハマりポイント」を一つ。
ONユニットは非常に便利ですが、「どこでエラーが起きても全部同じ処理をする」ようなグローバルすぎる書き方をすると、かえってデバッグ(原因調査)の時に「一体どこでゼロ除算が起きたんだ!?」と頭を抱えることになります。
- どのファイル、どのレコードの計算でゼロ除算が起きたのか、エラーログ(メッセージ)にキー項目を一緒に出力する。
- 許容できない致命的なゼロ除算の場合は、ONユニット内でメッセージを出力したあとに、意図的に `GOTO` や専用の終了ルーチンへ飛ばして安全にクローズ処理を行う。
こうした「一手間」を加えることで、PL/Iの強力な例外処理はさらに輝きを増します。
—
おわりに
いかがでしたでしょうか?
「PL/Iのゼロ除算トラップ」と言われると、なんだか難しそうな呪文のように聞こえたかもしれませんが、蓋を開けてみれば「エラーが起きたときのセーフティネットを優しく張る仕組み」に過ぎません。
他言語の経験があるあなたなら、その本質を理解するのはあっという間です。
レガシーシステムの構造は、恐れるものではなく、先人たちの知恵が詰まった美しい建造物です。怖がらずに、一つずつ紐解いていきましょうね。
それでは、次回のメインフレーム・ワンポイントレッスンでお会いしましょう!
