【PL/I学習|実務向け】PL/I の RECURSIVE 属性:再帰処理を安全に実装するための必須知識

導入:なぜ RECURSIVE 属性が重要なのか

メインフレームの世界では、古くからある資産の中に、再帰的な処理を実装したいというニーズがしばしば発生します。例えば、階層構造のデータを効率的に処理したり、複雑な計算アルゴリズムを簡潔に表現したりする場合です。しかし、PL/I のような言語では、プロシージャが自分自身を呼び出す(再帰する)ことを明示的に許可しない限り、意図した通りに動作しません。

この「RECURSIVE 属性」こそが、PL/I で安全かつ効率的に再帰処理を実装するための鍵となります。この属性を付与しないまま再帰呼び出しを行うと、変数が共有されてしまい、予期せぬ副作用やバグの原因となるため、その重要性は計り知れません。本記事では、PL/I における RECURSIVE 属性の基礎から、具体的な実装方法、そして現場で役立つ注意点までを解説します。

基礎知識:再帰処理とスタック、AUTOMATIC 変数

再帰処理とは、ある処理の定義の中に、その処理自身を呼び出す部分が含まれていることを指します。よく例に挙げられるのが、階乗の計算や、ツリー構造の走査(探索)です。

例えば、階乗 $n!$ は以下のように定義されます。

  • $n=0$ のとき、$n! = 1$
  • $n>0$ のとき、$n! = n \times (n-1)!$

この定義は、まさに再帰的です。$(n-1)!$ を計算する部分で、階乗を計算する処理自身を呼び出しています。

再帰処理を理解する上で欠かせないのが「スタック」と「AUTOMATIC 変数」の概念です。

  • スタック (Stack): プログラムの実行中に、関数の呼び出し情報(戻り先アドレス、ローカル変数など)を一時的に保存しておくためのメモリ領域です。関数が呼び出されるたびに情報がスタックに追加され(プッシュ)、関数から戻るたびに情報がスタックから削除されます(ポップ)。この LIFO (Last In, First Out) の性質が、再帰処理の各呼び出しを区別する上で重要になります。
  • AUTOMATIC 変数: PL/I において、プロシージャ内で宣言された変数のうち、デフォルトで AUTOMATIC 属性を持つものは、プロシージャが呼び出されるたびに新しいメモリ領域が確保されます。プロシージャから抜けると、そのメモリ領域は解放されます。

RECURSIVE 属性を付与すると、プロシージャが再帰的に呼び出されるたびに、そのプロシージャ内で宣言された AUTOMATIC 変数の新しいインスタンスがスタック上に確保されます。これにより、各再帰呼び出しは、それぞれ独立したローカル変数を持つことができ、互いに干渉することなく処理が進められます。

実装/解決策:RECURSIVE 属性の指定方法

PL/I でプロシージャに RECURSIVE 属性を付与するには、プロシージャ宣言の `PROCEDURE` キーワードの後に `RECURSIVE` を記述します。

構文例:

/ RECURSIVE 属性を持つプロシージャの宣言 /
MY_PROCEDURE: PROC(引数リスト) RECURSIVE;
/ プロシージャ本体 /
/ AUTOMATIC 変数宣言など /

/ 再帰呼び出し /
CALL MY_PROCEDURE(新しい引数);

END MY_PROCEDURE;

例:階乗計算

以下に、階乗を計算する PL/I プロシージャを RECURSIVE 属性付きで示します。

/ 階乗を計算する再帰プロシージャ /
FACTORIAL: PROC(N) RECURSIVE BINARY(63);
DCL N BINARY(63) INPUT; / 入力値 (非負整数) /
DCL RESULT BINARY(63); / 計算結果 /

/ ベースケース: N=0 または N=1 の場合 /
IF N <= 1 THEN RESULT = 1; ELSE / 再帰ステップ: N (N-1)! / / Factorial プロシージャ自身を再帰呼び出し / CALL FACTORIAL(N - 1) RETURNING RESULT; END; RETURN (RESULT); / 計算結果を返す / END FACTORIAL; / 呼び出し例 / MAIN: PROC OPTIONS(MAIN); DCL FACT_5 BINARY(63); / FACTORIAL プロシージャを呼び出す / FACT_5 = FACTORIAL(5); PUT SKIP LIST('5! =', FACT_5); / 出力: 5! = 120 / END MAIN; この例では、`FACTORIAL` プロシージャに `RECURSIVE` 属性が付与されています。`N` が 1 以下になるまで、`FACTORIAL` は自身を呼び出し続けます。各呼び出しでは、新しい `N` の値と、その時点での `RESULT` を持つ独立したスタックフレームが作成されます。

サンプルプログラム:木構造の走査(深さ優先探索)

ここでは、架空のツリー構造を走査する例を示します。各ノードは、その値と子ノードへのポインタ(ここでは簡略化して配列インデックスで表現)を持つと仮定します。

/ ツリー構造を深さ優先で走査する再帰プロシージャ /
TRAVERSE_TREE: PROC(NODE_INDEX) RECURSIVE;
/

  • NODE_INDEX: 現在走査しているノードのインデックス
  • このプロシージャは、指定されたノードとその全ての子孫ノードを
  • 訪問し、各ノードの値を表示します。

/

DCL NODE_INDEX FIXED BINARY; / 現在のノードのインデックス /

/ グローバル変数としてツリー構造を定義 (例) /
DCL 1 TREE,
2 NODE(10) BASED(ADDR(TREE)), / 最大10ノード /
3 VALUE CHAR(10),
3 CHILDREN(5) FIXED BINARY; / 子ノードのインデックス配列 /
DCL NULL_INDEX FIXED BINARY INIT(0); / 子が存在しないことを示す /

/ 現在のノードの値を取得 /
DCL CURRENT_VALUE CHAR(10);
CURRENT_VALUE = TREE.NODE(NODE_INDEX).VALUE;

/ 現在のノードの値を表示 /
PUT SKIP LIST(‘Visiting node:’, CURRENT_VALUE);

/ 子ノードが存在するか確認 /
DCL I FIXED BINARY; / ループカウンタ /
DCL CHILD_INDEX FIXED BINARY; / 子ノードのインデックス /

/ 子ノードを順に走査 (再帰呼び出し) /
DO I = 1 TO 5;
CHILD_INDEX = TREE.NODE(NODE_INDEX).CHILDREN(I);
/ 子ノードが存在する場合 (NULL_INDEX でない場合) /
IF CHILD_INDEX ^= NULL_INDEX THEN
/TRAVERSE_TREE プロシージャ自身を再帰呼び出し /
CALL TRAVERSE_TREE(CHILD_INDEX);
END;
END;

END TRAVERSE_TREE;

/ 呼び出し側のメインプログラム (ツリー構造の初期化と呼び出し) /
MAIN_TRAVERSE: PROC OPTIONS(MAIN);
/ ツリー構造の定義と初期化 /
DCL 1 TREE,
2 NODE(10) BASED(ADDR(TREE)),
3 VALUE CHAR(10),
3 CHILDREN(5) FIXED BINARY;
DCL NULL_INDEX FIXED BINARY INIT(0);

/ ツリーの構造を構築 (例) /
/ ルートノード /
TREE.NODE(1).VALUE = ‘Root’;
TREE.NODE(1).CHILDREN = (2, 3, NULL_INDEX, NULL_INDEX, NULL_INDEX);

/ 子ノード1 /
TREE.NODE(2).VALUE = ‘Child A’;
TREE.NODE(2).CHILDREN = (4, 5, NULL_INDEX, NULL_INDEX, NULL_INDEX);

/ 子ノード2 /
TREE.NODE(3).VALUE = ‘Child B’;
TREE.NODE(3).CHILDREN = (6, NULL_INDEX, NULL_INDEX, NULL_INDEX, NULL_INDEX);

/ 孫ノード /
TREE.NODE(4).VALUE = ‘Grandchild A1’;
TREE.NODE(4).CHILDREN = (NULL_INDEX, NULL_INDEX, NULL_INDEX, NULL_INDEX, NULL_INDEX);

TREE.NODE(5).VALUE = ‘Grandchild A2’;
TREE.NODE(5).CHILDREN = (NULL_INDEX, NULL_INDEX, NULL_INDEX, NULL_INDEX, NULL_INDEX);

TREE.NODE(6).VALUE = ‘Grandchild B1’;
TREE.NODE(6).CHILDREN = (NULL_INDEX, NULL_INDEX, NULL_INDEX, NULL_INDEX, NULL_INDEX);

/ ツリー走査を開始 (ルートノードのインデックスは 1) /
PUT SKIP LIST(‘Starting tree traversal…’);
CALL TRAVERSE_TREE(1);
PUT SKIP LIST(‘Traversal finished.’);

END MAIN_TRAVERSE;

このサンプルでは、`TRAVERSE_TREE` プロシージャが `RECURSIVE` 属性を持っています。各呼び出しで `NODE_INDEX` が変わり、そのノードの子ノードに対して自身を再帰的に呼び出します。これにより、ツリー構造を深さ優先で効率的に走査できます。`AUTOMATIC` 変数(この例では `CURRENT_VALUE` や `I`, `CHILD_INDEX` など)は、各呼び出しごとに独立して確保されるため、処理が混在することはありません。

応用・注意点:現場で役立つ補足情報と落とし穴

  • Java との比較: Java では、メソッドはデフォルトで再帰可能です。しかし、PL/I では `RECURSIVE` 属性を明示的に指定する必要があります。この言語仕様の違いを理解しておくことは重要です。
  • レガシー資産の改修時の注意: 既存の PL/I コードで `RECURSIVE` 属性がないプロシージャを、不用意に再帰可能にしようとすると、深刻なバグを引き起こす可能性があります。`RECURSIVE` 属性がない場合、プロシージャが自身を呼び出すと、同じ `AUTOMATIC` 変数領域が共有されてしまいます。これは、意図しない副作用(ある呼び出しで変更された変数が、別の呼び出しに影響を与える)の原因となります。

もし、レガシーコードを再帰化する必要がある場合は、そのコードの変数のスコープとライフサイクルを正確に把握し、`RECURSIVE` 属性を付与した上で、必要に応じて変数の管理方法を見直す必要があります。

  • スタックオーバーフロー: 再帰処理は、その性質上、深い再帰呼び出しを行うとスタック領域を大量に消費します。もし、再帰の終了条件が正しく設定されていなかったり、非常に深い階層を持つデータ構造を扱ったりすると、「スタックオーバーフロー」エラーが発生し、プログラムが異常終了する可能性があります。
  • 終了条件の確認: 再帰関数を実装する際は、必ずベースケース(再帰が終了する条件)を明確に定義し、それが確実に満たされるように設計してください。
  • 最大再帰深度の考慮: 可能な場合は、再帰の深さに上限を設けるか、イテレーション(繰り返し)処理に置き換えることも検討しましょう。PL/I の環境によっては、スタックサイズを調整できる場合もありますが、根本的な解決策ではありません。
  • 間接再帰: RECURSIVE 属性は、プロシージャが直接自分自身を呼び出すだけでなく、プロシージャ A がプロシージャ B を呼び出し、B が A を呼び出すといった「間接再帰」にも適用されます。ただし、間接再帰の場合、関与する全てのプロシージャに RECURSIVE 属性が必要になるわけではなく、呼び出しパスのいずれか、または全てのプロシージャに適用するかどうかは、その実装意図によります。一般的には、再帰的な呼び出し構造を持つ全てのプロシージャに RECURSIVE 属性を付与するのが安全です。

RECURSIVE 属性は、PL/I における強力な機能ですが、その挙動を正確に理解し、慎重に実装することが求められます。本記事が、皆さんのメインフレーム開発における再帰処理の実装の一助となれば幸いです。

コメント

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