こんにちは!メインフレームの荒波へようこそ。
JavaやCOBOLといった、現代的あるいはビジネスシーンでおなじみの言語をバリバリ書いてきた方にとって、IBMメインフレームの世界、そして「PL/I(ピーエルアイ)」という言語は、最初に少し身構えてしまう存在かもしれませんよね。
「なんだか変数名のルールが独特らしい…」
「ポインタや謎のデータ属性があって怖そう…」
そんな風に思っていませんか?でも、大丈夫ですよ。一つずつ蓋を開けて中身を覗いていけば、PL/Iは非常に合理的で、かつての人たちが知恵を絞って作り上げた「美しい巨大なエンジン」のようなものだと分かります。
今回は、そんなPL/Iの世界への第一歩として、「識別子の命名規則と予約語を持たない自由な世界」に少し触れたあと、実務のバッチ処理や金融計算の改修で絶対に避けて通れない「ROUND関数の精度制御と丸め誤差の回避」について、じっくりと紐解いていきたいと思います。
肩の力を抜いて、コーヒーでも飲みながら読んでいってくださいね。
—
1. PL/Iって実は優しい?「予約語」がない世界のハナシ
JavaやCOBOLを触ってきた方だと、こんな経験があるのではないでしょうか。
「あ、この英単語、言語のキーワード(予約語)だから変数名に使えないや……別の名前にしよっと」
プログラミング言語の多くは、文法をコンパイラが正しく理解するために、「IF」や「READ」「SUM」といった単語を予約語としてガチガチに固めています。
ところが、PL/Iには「厳密な意味での予約語」がありません。
「えっ、じゃあ `IF` を変数名にしちゃったら、コンパイラが混乱しないの?」と思いますよね。そこがPL/Iの面白いところであり、少しレガシーな愛嬌のあるところです。PL/Iのコンパイラは、文脈(コンテキスト)を見て判断します。
1
/ PL/Iの驚きの世界 /
IF = 10; / これ、変数名としての ‘IF’ です /
IF IF = 20 THEN / こっちは制御文の ‘IF’ です /
/ 処理がここに入ります /
人間が読んだら「なんて読みにくいんだ!」と頭を抱えたくなりますが、コンパイラは前後の文脈でちゃんと「おっ、ここでのIFは変数だな」「こっちは条件分岐のキーワードだな」と見分けてくれます。
とはいえ、実務の現場でこんな書き方をしたら、コードレビューで先輩からこってり絞られます(笑)。実務ではもちろん、分かりやすい変数名をつけますが、「言語仕様としてガチガチに縛られていない柔軟な言語なんだな」と知っておくだけで、PL/Iに対する恐怖心が少し薄れるのではないでしょうか。
—
2. 金融システムの命運を握る「ROUND関数」の正体
さて、ここからが今回の本題です。
基幹システム、特に銀行や保険などの勘定系システムにおいて、お金の計算(金利計算や手数料算出など)は1円の狂いも許されないシビアな世界です。
Javaなら `BigDecimal` を使って丸めモードを指定したり、COBOLなら `ROUNDED` 句を使ったりしますよね。では、PL/Iではどうするのでしょうか?ここで登場するのが `ROUND` 関数 です。
しかし、メインフレームのPL/Iで使われる固定小数点数(`FIXED DECIMAL`など)の計算では、何も考えずに丸めを行っていると、私たちが意図しない「丸め誤差」や「桁落ち」という魔物が忍び寄ってきます。
データのストレージ属性と「内部的な世界」
PL/Iでよく使われる数値データ型に `FIXED DECIMAL` があります。
例えば、以下のような宣言をよく見かけます。
1
DCL WK-AMOUNT FIXED DECIMAL(11, 2) ; / 全11桁、うち小数点以下2桁 /
これ、COBOLでいう `PIC S9(9)V99 COMP-3` と同じ、パック十進数(Packed Decimal)としてメインフレームのメモリ(またはストレージ)に格納されます。人間にとっては10進数で扱いやすいのですが、コンピュータのCPUは本来、2進数の世界です。
この10進数をかけ算したり割り算したりするとき、PL/Iの内部ではどのようなことが起きているのでしょうか?
1. 乗算・除算による桁あふれの防止
例えば、小数点以下2桁同士を掛け合わせると、数学的には小数点以下4桁になります。PL/Iは精度を落とさないよう、内部的に一時的な作業領域(レジスタや拡張ワークエリア)でより精度の高い状態を保ちます。
2. シフトと加算の組み合わせによる丸め
指定された桁数(例えば小数点以下2桁)に戻す際、PL/Iのランタイムは単に切り捨てるのではなく、指定桁の「1つ下の桁」に `5` に相当する値を加算(四捨五入の場合)してから、不要な下位桁をシフト(切り捨て)するアルゴリズムを内部的に実行します。
「あれ、値がズレる?」丸め誤差を生む落とし穴
この内部的な加算とシフトの仕組み、非常に理にかなっているのですが、「割り算」が絡むと牙を剥きます。
例えば、3で割るような割り切れない割算をしてみましょう。
1
DCL RESULT FIXED DECIMAL(5, 2);
DCL RATE FIXED DECIMAL(5, 4);
RATE = 1 / 3; / 0.3333 になる /
RESULT = ROUND(RATE 100, 2); / さあ、どうなる? /
もし、中間計算の段階で有効桁数が途中で丸められてしまうと、その累積が「誤差」となって、日次バッチの最終的な合計金額が「1円合わない(いわゆる端数ズレ)」という、メインフレームエンジニアが最も恐れる現象を引き起こします。
—
3. 怖くない!丸め誤差を回避する実践的コーディングパターン
では、このレガシーな世界で、どのように精度を維持し、丸め誤差を華麗に回避すればよいのでしょうか。実務で使える安全なコーディングのポイントを、サンプルコードとともに見ていきましょう。
実践コード:安全な丸め処理と精度の維持
1
————————————————————–
- プログラム名: CALC01
- 概要 : 割算とROUND関数を用いた安全な金額計算のサンプル
————————————————————–
CALC01: PROC OPTIONS(MAIN);
/ 変数の宣言:FIXED DECIMALで精度を明示的に大きく取る /
DCL W-BASE-AMT FIXED DECIMAL(13, 2) INIT(1000.00); / 元金 /
DCL W-RATE FIXED DECIMAL(7, 5) INIT(0.03333); / 利率 /
DCL W-CALC-WRK FIXED DECIMAL(15, 6); / 中間計算用(余裕を持たせる) /
DCL W-FINAL-AMT FIXED DECIMAL(11, 2); / 最終結果 /
/ 1. 中間計算はできる限り精度を落とさない(桁数を大きく保つ) /
/ × W-FINAL-AMT = W-BASE-AMT W-RATE; (これだと精度が落ちる) /
W-CALC-WRK = W-BASE-AMT W-RATE;
/ 2. ROUND関数を使う際は、丸める直前まで十分な小数点以下の桁数を維持する /
/ ROUND( 表达式, 戻したい小数点以下の桁数 ) /
W-FINAL-AMT = ROUND(W-CALC-WRK, 2);
/ 結果の確認用出力 /
PUT SKIP EDIT (‘計算結果: ‘, W-FINAL-AMT) (A, F(10,2));
END CALC01;
アーキテクトからのワンポイントアドバイス
1. 中間ワークは「ケチらず大きく」宣言する
PL/Iの計算精度は、代入される側の変数だけでなく、「計算式の右辺(中間結果)がどのデータ属性で評価されるか」に強く依存します。右辺の計算途中で桁あふれや意図しない切り捨てが起きないよう、中間ワーク(上記の `W-CALC-WRK` のように)は小数点以下の桁数を多め(例: 6桁や8桁)に確保するのが鉄則です。
2. ROUND関数は「最後の砦」として使う
計算の途中で何度も `ROUND` を挟むと、その都度内部的な加算と丸め(シフト)が行われ、誤差が積み重なります。`ROUND` は、すべての計算が終わった「最後の出力直前」に1回だけ通すのが、誤差を回避する最も美しいアプローチです。
—
おわりに
いかがでしたでしょうか?
予約語がないという少し変わった気質を持ちながらも、PL/Iはデータの精度を極限までコントロールできる、非常に頼もしい言語です。
「内部でどんな加算やシフトが行われているか」を少しだけイメージできるようになると、レガシーなコードを眺める視点もガラリと変わるはずです。
メインフレームのモダナイゼーションや保守開発でPL/Iに出会ったとき、「うわ、難しそう…」と逃げ出したくなったときは、この記事の「内部の仕組み」を思い出してみてくださいね。
それでは、次回のメインフレーム・アーキテクチャでお会いしましょう!
