こんにちは!世界最高峰のメインフレームシステムアーキテクト、そしてPL/Iの沼にどっぷり浸かったレガシー移行スペシャリストの私がお届けする技術ブログへようこそ!
JavaやCOBOLといった現代的、あるいはメジャーな言語での開発経験はおありでも、IBMメインフレームとPL/Iの世界は初めて、という方もいらっしゃるかもしれませんね。一見すると「古めかしい」「難解そう」と感じるかもしれませんが、ご安心ください。PL/Iは非常にパワフルで、その奥深さを知れば知るほど、きっとその魅力に引き込まれるはずです。
今回は、PL/Iの数値計算において、特に多くのシステムでバグの温床になりがちな「桁落ち」や「精度喪失」という、ちょっとした落とし穴について深く掘り下げていきます。特に`BINARY FIXED`と`DECIMAL FIXED`という二つのデータ型が絡む際に起こる、PL/Iならではの振る舞いを、現場での実例を交えながら優しく紐解いていきましょう。
—
PL/Iへようこそ!数値計算の奥深さを体験しよう
Javaの`int`や`long`、COBOLの`PIC 9(n)`など、数値データ型はどの言語にもありますよね。PL/Iでももちろん数値を扱うためのデータ型が用意されていますが、その宣言の仕方が少しユニークなんです。
PL/Iの数値データ型は、主に以下の3つの要素で構成されます。
1. 基数 (BASE): 数値を2進数で扱うか(`BINARY`)、10進数で扱うか(`DECIMAL`)
2. 形式 (SCALE): 数値を整数として扱うか(`FIXED`)、浮動小数点数として扱うか(`FLOAT`)
3. 精度 (PRECISION): 何桁の数値を扱えるか、小数点以下の桁数も含む
今回はこの中でも特に、基数が異なる`BINARY FIXED`と`DECIMAL FIXED`という、どちらも「整数」を扱うデータ型に焦点を当てていきます。この二つが混在する計算で、「あれ?なんか結果がおかしいぞ?」となるケースが意外と多いんですよ。
PL/Iの識別子(変数名)と予約語のルールって?
本題に入る前に、PL/Iのちょっとした「おもしろルール」に触れておきましょう。PL/Iには、JavaやCOBOLのような「予約語」という概念が、ほとんどありません。例えば、COBOLで`MOVE`を変数名にしたり、Javaで`class`を変数名にしたりすることはできませんよね?
でもPL/Iでは、例えば`READ`とか`WRITE`といった、他の言語なら予約語になりそうな単語でも、変数名として使えちゃうんです。
PLI
DCL READ BINARY FIXED (15); / ‘READ’という変数が宣言できてしまう! /
DCL WRITE DECIMAL FIXED (5,2); / ‘WRITE’もOK /
READ = 100;
WRITE = READ 1.5;
PUT SKIP LIST (‘WRITE = ‘, WRITE); / 実際はこんなコードは書きませんが、理論上は可能 /
これはPL/Iのコンパイラが、文脈からそれがキーワードなのか変数名なのかを判断してくれるためです。非常に自由度が高い反面、可読性が落ちたり、思わぬバグに繋がったりすることもあるので、推奨はされません。基本的には、意味のあるユニークな変数名を付けるのが鉄則ですよ。
—
BINARY FIXEDとDECIMAL FIXEDって、結局何が違うの?
さあ、本題のデータ型に戻りましょう。
BINARY FIXED (p)
- 内部表現: コンピューターが最も得意とする2進数(バイナリ)で数値を表現します。
- 特徴: 演算が非常に高速です。しかし、人間にとっては2進数を直接見ることは稀なので、デバッグ時などは少しとっつきにくいかもしれません。
- 宣言例: `DCL COUNT BINARY FIXED (31);`
- `31`という数値は、この変数が最大で31ビットの2進数で表現されることを意味します。符号ビットを考慮すると、約-20億から+20億までの整数を扱えます。Javaでいう`int`型に近いイメージですね。
DECIMAL FIXED (p, q)
- 内部表現: 人間が普段使う10進数(デシマル)で数値を表現します。メインフレームでは「パック10進数」という形式で効率的に格納されることが多いです。
- 特徴: 桁落ちの心配が少なく、人間が直感的に理解しやすい形式です。帳票出力や画面表示など、人間に見せる数値データによく使われます。計算はBINARYに比べて少しだけ遅くなります。
- 宣言例: `DCL AMOUNT DECIMAL FIXED (7,2);`
- `7`は数値全体の桁数(精度)、`2`は小数点以下の桁数を意味します。この場合、`99999.99`まで(符号を考慮しなければ)の数値を扱えます。COBOLの`PIC S9(5)V9(2) COMP-3`のようなイメージです。`DECIMAL FIXED (5,0)`なら`PIC S9(5) COMP-3`と同じ感覚で使えますね。
なぜこの2つが問題になりやすいのか?
一見、`BINARY FIXED`は高速、`DECIMAL FIXED`は人間向き、と棲み分けができているように見えますよね。問題は、この異なる基数のデータ型同士を混ぜて演算した場合に発生します。
PL/Iは「賢い」ので、異なるデータ型を混ぜて計算しようとすると、自動的にどちらかのデータ型に「変換」してから演算を実行してくれます。これを「暗黙の型変換(Implicit Conversion)」と呼びます。
この「賢さ」が、時として私たちプログラマを悩ませる「罠」になることがあるんです。PL/Iは、可能な限り精度を維持しようとしながらも、演算結果を格納する変数の属性に合わせようとする、という複雑なルールで動いています。
—
計算誤差の罠!精度切り捨てとオーバーフローの現場
それでは、実際にコード例を見ながら、何が起こるのかを見ていきましょう。
罠その1:乗算での桁落ち(オーバーフロー)
`BINARY FIXED`と`DECIMAL FIXED`を掛け合わせた場合、結果がどうなるか見てみましょう。
PLI
DCL BINARY_VALUE BINARY FIXED (15); / BINARY FIXED (15) は最大32767 /
DCL DECIMAL_VALUE DECIMAL FIXED (5,0); / DECIMAL FIXED (5,0) は最大99999 /
DCL RESULT_VALUE DECIMAL FIXED (5,0); / 結果を格納。最大99999 /
/ ——————————————————————- /
/ 例1: 意図しない桁落ちが発生するケース /
/ ——————————————————————- /
BINARY_VALUE = 1000;
DECIMAL_VALUE = 12345;
/ BINARY_VALUE DECIMAL_VALUE の結果は 1000 12345 = 12345000 です /
/ この結果を RESULT_VALUE (DECIMAL FIXED 5,0) に格納しようとすると… /
RESULT_VALUE = BINARY_VALUE DECIMAL_VALUE;
PUT SKIP LIST (‘RESULT_VALUE (DECIMAL FIXED 5,0) = ‘, RESULT_VALUE);
/ 期待値は 12345000 ですが、実際にはオーバーフローが発生し、 /
/ 例えば0になったり、プログラムが異常終了したりする可能性があります。 /
/ メインフレームの環境やコンパイラのオプションによって挙動は変わりますが、/
/ 少なくとも「期待通りの値」にはなりません。 /
/ ——————————————————————- /
/ 例2: 桁落ちを防ぐための適切な宣言 /
/ ——————————————————————- /
DCL SAFE_RESULT_VALUE DECIMAL FIXED (11,0); / 12345000 は8桁なので、11桁あれば十分 /
BINARY_VALUE = 1000;
DECIMAL_VALUE = 12345;
SAFE_RESULT_VALUE = BINARY_VALUE DECIMAL_VALUE;
PUT SKIP LIST (‘SAFE_RESULT_VALUE (DECIMAL FIXED 11,0) = ‘, SAFE_RESULT_VALUE);
/ この場合は、12345000 が正しく格納され、表示されます。 /
解説:
1. `BINARY FIXED (15)`は約`DECIMAL FIXED (5,0)`と同等の精度を持ちます。(`CEIL(15 LOG10(2))`で計算すると約4.51なので、5桁相当)
2. PL/IのFIXED-point乗算では、演算結果の精度は、オペランドの精度を足し合わせたものよりも大きくなる傾向があります。具体的には、`(p1+p2+1, q1+q2)`のようなルールで計算されます。
- `BINARY_VALUE`は`DECIMAL(5,0)`相当、`DECIMAL_VALUE`は`DECIMAL(5,0)`。
- この2つの乗算の結果は、PL/Iの内部で`DECIMAL FIXED (5+5+1, 0+0)`、つまり`DECIMAL FIXED (11,0)`という大きな精度で計算されます。
3. この`DECIMAL FIXED (11,0)`の結果(`12345000`)を、`DECIMAL FIXED (5,0)`で宣言された`RESULT_VALUE`に代入しようとすると、`RESULT_VALUE`の容量が足りません。
4. この場合、PL/Iでは`FIXEDOVERFLOW`という条件が発生し、プログラムが中断されたり、あるいはシステム規定値(0など)が格納されてしまったりします。これはまさに「バグ」ですよね。
罠その2:除算での小数点以下切り捨て
割り算はさらに注意が必要です。小数点以下が発生する可能性があるため、PL/Iは可能な限り小数点以下も保持しようとします。しかし、格納する変数が小数点以下を定義していない場合、問答無用で切り捨てられます。
PLI
DCL NUMERATOR BINARY FIXED (31); / 2進数31桁 (約10進数9桁) /
DCL DENOMINATOR DECIMAL FIXED (5,0); / 10進数5桁、小数点以下0桁 /
DCL RESULT_INT DECIMAL FIXED (5,0); / 結果を格納 (整数部のみ) /
DCL RESULT_DEC DECIMAL FIXED (7,2); / 結果を格納 (小数点以下2桁まで) /
DCL RESULT_DEC_LONG DECIMAL FIXED (15,5); / 結果を格納 (小数点以下5桁まで) /
/ ——————————————————————- /
/ 例3: 割り算で小数点以下が切り捨てられるケース /
/ ——————————————————————- /
NUMERATOR = 100;
DENOMINATOR = 3;
/ 100 / 3 = 33.33333… です /
RESULT_INT = NUMERATOR / DENOMINATOR;
PUT SKIP LIST (‘RESULT_INT (DECIMAL FIXED 5,0) = ‘, RESULT_INT);
/ 出力: RESULT_INT (DECIMAL FIXED 5,0) = 33 /
/ 小数点以下が切り捨てられてしまいました。 /
RESULT_DEC = NUMERATOR / DENOMINATOR;
PUT SKIP LIST (‘RESULT_DEC (DECIMAL FIXED 7,2) = ‘, RESULT_DEC);
/ 出力: RESULT_DEC (DECIMAL FIXED 7,2) = 33.33 /
/ 小数点以下2桁までは保持されました。 /
RESULT_DEC_LONG = NUMERATOR / DENOMINATOR;
PUT SKIP LIST (‘RESULT_DEC_LONG (DECIMAL FIXED 15,5) = ‘, RESULT_DEC_LONG);
/ 出力: RESULT_DEC_LONG (DECIMAL FIXED 15,5) = 33.33333 /
/ ここで初めて「期待通り」に近い値が得られました。 /
解説:
1. `BINARY FIXED (31)` と `DECIMAL FIXED (5,0)` の割り算です。ここでもPL/Iは内部で最適な精度を持つ中間結果を生成しようとします。一般的に、割り算の結果は小数点以下を持つ可能性があります。
2. `RESULT_INT`は`DECIMAL FIXED (5,0)`と宣言されているため、小数点以下を保持する領域がありません。そのため、`33.333…`という中間結果の小数点以下は問答無用で切り捨てられ、`33`が格納されます。
3. `RESULT_DEC`は`DECIMAL FIXED (7,2)`と宣言されているため、小数点以下2桁まで保持できます。`33.333…`から`33.33`が格納されます。
4. `RESULT_DEC_LONG`のように、十分な精度を確保すれば、より正確な結果を得られます。
この挙動は、特に金額計算や比率計算で致命的なバグにつながりやすいです。「計算結果が1円違う!」なんて、基幹システムでは大問題になりますよね。
—
現場で役立つ!解決策とベストプラクティス
では、このようなPL/I特有の「罠」を避けて、安全な数値計算を行うためにはどうすれば良いのでしょうか?
1. 明示的な型変換(Built-in Functions)を活用する
PL/Iには、型を明示的に変換するための組み込み関数が用意されています。これらを活用することで、PL/Iの暗黙の変換ルールに頼らず、プログラマの意図通りに変換を制御できます。
- `DEC(expression, p, q)`: 式を10進固定小数点数に変換します。`p`は全体の桁数、`q`は小数点以下の桁数です。
- `BIN(expression, p)`: 式を2進固定小数点数に変換します。`p`はビット数です。
- `PRECISION(expression, p, q)`: 式の精度を指定して変換します。
先ほどの乗算の例を、明示的な型変換を使って修正してみましょう。
PLI
DCL BINARY_VALUE BINARY FIXED (15);
DCL DECIMAL_VALUE DECIMAL FIXED (5,0);
DCL SAFE_RESULT_VALUE DECIMAL FIXED (11,0); / 適切な精度を確保 /
BINARY_VALUE = 1000;
DECIMAL_VALUE = 12345;
/ 乗算結果が DECIMAL FIXED (11,0) になるように明示的に変換 /
SAFE_RESULT_VALUE = DEC(BINARY_VALUE DECIMAL_VALUE, 11, 0);
PUT SKIP LIST (‘SAFE_RESULT_VALUE (DECIMAL FIXED 11,0, with DEC func) = ‘, SAFE_RESULT_VALUE);
/ 出力: SAFE_RESULT_VALUE (DECIMAL FIXED 11,0, with DEC func) = 12345000 /
/ 期待通りの値が得られました。 /
この例では結果変数の精度が既に適切なので、`DEC`関数は必須ではありませんが、中間結果を特定の精度で確定させたい場合などに非常に有効です。
2. 結果を格納する変数の精度を十分に確保する
これは最も基本的ながら、非常に重要なポイントです。計算結果が最大でどのくらいの値になるのか、小数点以下は何桁まで必要か、を事前に把握し、それに耐えうる精度で変数を宣言しましょう。特に、中間結果を変数に代入する場合は要注意です。
- 乗算: `DECIMAL FIXED(p1,q1)` と `DECIMAL FIXED(p2,q2)` の乗算結果は、通常 `DECIMAL FIXED(p1+p2+1, q1+q2)` 程度の精度が必要です。
- 除算: 小数点以下の精度をどれだけ保持したいかによって、`q`の値を適切に設定します。
3. `FIXEDOVERFLOW`条件をハンドリングする
PL/Iには、計算結果が宣言された精度を超えた場合に発生する`FIXEDOVERFLOW`という条件があります。これを`ON FIXEDOVERFLOW`文でハンドリングすることで、プログラムの異常終了を防ぎ、エラー処理を記述できます。
PLI
DCL BINARY_VALUE BINARY FIXED (15);
DCL DECIMAL_VALUE DECIMAL FIXED (5,0);
DCL RESULT_VALUE DECIMAL FIXED (5,0); / 意図的に小さい精度 /
ON FIXEDOVERFLOW BEGIN;
PUT SKIP LIST (‘!!! FIXEDOVERFLOW CONDITION OCCURRED !!!’);
RESULT_VALUE = 99999; / エラー発生時のデフォルト値を設定するなど /
/ あるいはエラーログを出力してプログラムを終了するなどの処理 /
END;
BINARY_VALUE = 1000;
DECIMAL_VALUE = 12345;
RESULT_VALUE = BINARY_VALUE DECIMAL_VALUE;
PUT SKIP LIST (‘RESULT_VALUE after overflow handling = ‘, RESULT_VALUE);
/ 出力:
!!! FIXEDOVERFLOW CONDITION OCCURRED !!!
RESULT_VALUE after overflow handling = 99999
となります。異常終了は防げますが、結果は不正です。
/
この例のように、`FIXEDOVERFLOW`はエラーを検知して対処するためのもので、根本的な精度不足を解消するものではない、という点にご注意ください。
4. 可能な限り、同じ基数で統一する
これは現場での鉄則とも言えます。異なる基数(`BINARY`と`DECIMAL`)を混ぜて演算すると、PL/Iの暗黙の変換ルールが適用され、予期せぬ結果を招くリスクが高まります。
もし可能であれば、演算に関わる全ての変数を`DECIMAL FIXED`で統一するか、あるいは全て`BINARY FIXED`で統一する、という方針を立てると、このような問題は格段に減ります。特に、金額や数量など、正確な10進数表現が求められるデータは、迷わず`DECIMAL FIXED`を使用しましょう。
5. 「なんか計算結果がおかしい」と思ったら、まずデータ型の精度と基数を疑え!
これは、レガシーシステムのバッチ改修やマイグレーション調査で遭遇する「あるある」です。COBOLのように`PIC`句で厳密に桁数が定義されているのと異なり、PL/Iは数値型の宣言に「自由度」がある分、プログラマがその「責任」を負う部分も大きくなります。
「なぜか端数が出る」「合計値が合わない」「異常終了する」といったトラブルが発生した場合、真っ先に疑うべきは、計算に関わる変数の基数と精度です。特に`BINARY`と`DECIMAL`が混在している箇所は、徹底的に調査することをお勧めします。
—
まとめ:PL/Iの数値型は怖くない!
PL/Iの`BINARY FIXED`と`DECIMAL FIXED`の間の数値変換ルールは、一見複雑で「怖い」と感じるかもしれません。しかし、その根底には「可能な限り精度を維持しようとする」というPL/Iの賢い振る舞いがあります。
今回ご紹介した「精度切り捨ての罠」や「オーバーフロー」は、PL/Iの言語仕様を理解し、適切なデータ型宣言と明示的な型変換を行うことで、十分に回避できます。
レガシーシステムは、長年の運用を経て、その仕様やバグが「文化」になっていることもあります。しかし、一つ一つの仕様を紐解き、その背景にあるロジックを理解すれば、決して難しいものではありません。
PL/Iの世界は奥深く、知れば知るほど面白いですよ!これからも一緒に、レガシーシステムの扉を開いていきましょう!
