チューリングマシンにおけるサブルーチンの実現:複雑なタスクを構造化する設計
はじめに
チューリングマシンは計算の基本的なモデルであり、そのシンプルな構造にもかかわらず、現代のコンピュータで計算可能なあらゆる問題を解く能力を持つとされています(チューリング完全性)。しかし、複雑な問題を解くチューリングマシンを設計しようとすると、その状態遷移規則は非常に大規模かつ複雑になりがちです。
現代のプログラミングにおいて、複雑な問題を扱う際には、全体を小さな部品(関数や手続き、サブルーチン)に分割し、それらを組み合わせていくという手法が不可欠です。この「構造化」の考え方は、チューリングマシンの設計においても有効でしょうか。チューリングマシンにサブルーチンという概念は存在するのでしょうか。
本記事では、チューリングマシンにおけるサブルーチンの考え方と、それをテープや状態を用いて具体的に実現するための設計方法について解説します。複雑なチューリングマシンをより分かりやすく、効率的に設計するためのヒントを提供し、シミュレーターでの具体的な動作を通して理解を深めることを目指します。
チューリングマシンにおけるサブルーチンの概念
プログラミングにおけるサブルーチンは、特定のタスクを実行する独立したコードブロックであり、プログラムの様々な箇所から呼び出すことができます。チューリングマシンにおいては、このような独立した処理ブロックを、特定の「状態群」とそれに対応する「遷移規則」の集合として表現します。
サブルーチンを呼び出す際には、通常、何らかの入力(引数)を渡し、サブルーチンは処理結果(戻り値)を返します。チューリングマシンでは、これらの情報のやり取りを主にテープ上のデータの配置とヘッドの位置によって行います。
また、サブルーチンが終了した後に、呼び出し元に戻って処理を継続する必要があります。プログラミングでは実行スタックなどを使って戻り先アドレスを記憶しますが、チューリングマシンには直接的なスタック機構はありません。これを実現するためには、テープ上に一時的な情報を書き込むなどの工夫が必要になります。
サブルーチン実現のための設計要素
チューリングマシンでサブルーチンを実現するためには、以下の要素を考慮して設計を行います。
-
テープの利用:
- 入力(引数)の配置: サブルーチンが処理を開始する前に、サブルーチンが必要とするデータをテープ上の特定の場所に配置します。ヘッドをそのデータの開始位置に移動させることも重要です。
- 出力(戻り値)の配置: サブルーチンは計算結果をテープ上の特定の場所に書き込みます。
- 作業領域: サブルーチン内部で一時的にデータを保存したり操作したりするための領域が必要です。
- 戻り先アドレスの記憶: これがチューリングマシンにおけるサブルーチンの実現で最も工夫が必要な点の一つです。最も単純な場合は、呼び出し元が一つしかなく、サブルーチン完了後は常に決まった状態に戻るように設計できます。しかし、複数の場所から同じサブルーチンを呼び出す場合、サブルーチン終了後にどの呼び出し元に戻るかを記憶する必要があります。テープ上の特定の場所に呼び出し元を識別する記号(例えば、戻るべきメインルーチンの状態番号を示す記号や、テープ上の特定のマークなど)を一時的に書き込んでおく、という方法が考えられます。
-
状態の設計:
- メインルーチンの状態群: サブルーチンを呼び出す処理を含む、主たる計算を行う状態の集合です。
- サブルーチン呼び出し状態: メインルーチンの中で、サブルーチンに処理を移すための状態です。この状態からサブルーチンの開始状態へ遷移します。その際に、テープ上の戻り先情報などをセットする処理を含める場合があります。
- サブルーチン本体の状態群: サブルーチンが担当する特定のタスクを実行するための一連の状態の集合です。
- サブルーチン終了状態: サブルーチン本体の処理が完了したことを示す状態です。この状態から呼び出し元に戻るための処理(戻り値の整理、戻り先情報の読み取りと対応する状態への遷移など)を行います。
-
遷移関数の設計:
- 特定の記号を読み込んだ際に、サブルーチン呼び出し状態を経てサブルーチン開始状態へ遷移するルールを定義します。
- サブルーチン本体内の計算を行うための遷移ルールを定義します。
- サブルーチンが終了条件を満たした際に、サブルーチン終了状態から、テープ上の戻り先情報に基づいて適切なメインルーチンの状態へ遷移するルールを定義します。
このように、チューリングマシンにおけるサブルーチンは、状態のグループ化と、テープ上での情報(引数、戻り値、戻り先)の受け渡し・記憶によって実現されます。
具体例:単項表現インクリメントサブルーチンを持つチューリングマシン
ここでは、簡単なサブルーチンを持つチューリングマシンの例として、「テープ上の複数の単項表現数値(例: 111#11#1
)を順にインクリメントするマシン」を考えます。各単項表現のインクリメント処理をサブルーチンとして実装します。単項表現とは、数値 n
を 1
が n
個並んだ文字列で表現する方法です(例: 3 は 111
、2 は 11
、1 は 1
)。数値は #
で区切られ、テープの端は空白(_
)とします。
このマシンは、テープの左端から右に進み、#
または空白を見つけるたびに、その直前の単項表現をインクリメントするというタスクを実行します。インクリメント処理はサブルーチンとして設計します。
サブルーチンの仕様(単項表現インクリメント):
* 呼び出し時: ヘッドはインクリメント対象の単項表現の末尾(またはその直後の #
や空白)に位置しているとします。
* 処理内容: 対象の単項表現の末尾に 1
を一つ追加します。
* 戻り時: 処理を終え、ヘッドは元のインクリメント対象だった単項表現の元の末尾位置(インクリメント前の長さの末尾)に戻るように設計します。また、どこから呼び出されたか(すなわち、次にメインルーチンのどの処理に戻るべきか)を示す情報を処理します。今回は簡単のため、テープ上に呼び出し元を識別するマークを書き込んで戻り先を記憶する方法を採用します。
メインルーチンの処理概要:
1. テープの左端から開始します。
2. 右へ移動し、#
または空白に到達するまで 1
を読み飛ばします。
3. #
または空白を読み込んだら、直前の単項表現の末尾であると判断し、インクリメントサブルーチンを呼び出します。
4. サブルーチンから戻ってきたら、次の単項表現の開始位置(またはテープの終端)へ移動し、2へ戻ります。
5. 全ての単項表現を処理し終えたら停止します。
サブルーチンの処理概要(インクリメント&戻り先記憶):
1. #
または空白を読み込んだ状態から開始します。
2. テープ上の現在の位置に、メインルーチンの戻り先を示す一時的なマーク(例: R
)を書き込み、ヘッドを左へ移動します。
3. 左へ移動しながら、インクリメント対象の単項表現の開始位置(_
の次またはテープの左端)まで移動します。
4. 右へ移動し、単項表現の末尾(最初の _
)を探します。
5. 末尾の _
を 1
に書き換え、ヘッドを左へ移動します。これでインクリメントは完了です。
6. 左へ移動しながら、テープ上のマーク R
を探します。
7. マーク R
を見つけたら、それを消去し、ヘッドを右へ移動します。
8. メインルーチンの適切な状態(マークの直後の位置に対応する状態)へ遷移し、処理を戻します。
この設計では、テープが _111#11#1_
のような状態から開始すると、最終的に _1111#111#11_
のようになります。
シミュレーターでの動作解説
このチューリングマシンをシミュレーターで実行する際の動作を追ってみましょう。
例えば、初期テープが _11#1_
であるとします。
- 初期状態 (例: q0): ヘッドは左端の
_
にあります。右へ移動します。 - メインルーチン (例: q_main_scan):
1
を読み飛ばしながら右へ移動します。_
->1
->1
->#
とヘッドが移動します。状態はq_main_scan
のままです。 - サブルーチン呼び出し (例: q_call_inc):
#
を読み込みました。これは最初の単項表現11
の終わりを示しています。ここで状態をサブルーチン呼び出しの状態q_call_inc
に遷移させます。遷移規則に従い、テープの#
を戻り先マークR
に書き換え、ヘッドを左へ移動します。テープは_11R1_
、状態はq_call_inc
からサブルーチンの開始状態q_inc_start
へ遷移します。- シミュレーターでは: 現在の状態が
q_call_inc
に変わり、読み込んだ記号と遷移規則が表示されます。テープ上の#
がR
に変わり、ヘッドが一つ左に移動した様子が視覚的に確認できます。次の状態としてq_inc_start
が表示されます。
- シミュレーターでは: 現在の状態が
- サブルーチン本体 (例: q_inc_scan_left, q_inc_scan_right, q_inc_write):
q_inc_start
から左へ移動し、単項表現の開始位置(_
)を探します (q_inc_scan_left
状態)。_
を見つけたら右へ移動し、単項表現の末尾(最初の_
)を探します (q_inc_scan_right
状態)。- 単項表現の末尾
_
を見つけたら、状態をq_inc_write
に遷移させ、_
を1
に書き換え、ヘッドを左へ移動します。テープは_111R1_
となります。これでインクリメントは完了です。 - シミュレーターでは: 状態が
q_inc_scan_left
->q_inc_scan_right
->q_inc_write
と変化し、ヘッドが左右に動きながらテープの内容が_
から1
に変化する様子がステップごとに追えます。
- サブルーチンからの戻り (例: q_inc_return): インクリメント完了後、ヘッドは左へ移動し、テープ上のマーク
R
を探します (q_inc_return
状態)。R
を見つけたら、状態を遷移させ、R
を元の#
に戻し、ヘッドを右へ移動します。- シミュレーターでは: 状態が
q_inc_return
に変わり、ヘッドが左へ移動してR
を見つけ、それが#
に戻り、ヘッドが右へ移動する動作が見られます。
- メインルーチンへの復帰 (例: q_main_scan):
R
を#
に戻し右移動した後、ヘッドはテープ上の1
の位置にいます。これは、最初の単項表現のインクリメントが完了し、次の単項表現に進むべき位置です。状態は再びメインルーチンのq_main_scan
に戻ります。- シミュレーターでは: 状態が再び
q_main_scan
に戻り、ヘッドが次の処理位置にいることが確認できます。
- シミュレーターでは: 状態が再び
- 次の単項表現の処理:
q_main_scan
状態から、引き続き右へ移動し、次の#
または空白を探します。今回はテープの末尾の_
を見つけます。 - 再度サブルーチン呼び出し: 末尾の
_
を読み込み、再びサブルーチン呼び出し状態q_call_inc
へ遷移します。_
に戻り先マークR
を書き込み、左へ移動します。テープは_111#1R
となります。 - サブルーチン本体実行: 同様に左へ移動し、単項表現
1
の開始位置を探し、右へ移動して末尾の_
を1
に書き換えます。テープは_111#11R
となります。 - サブルーチンからの戻り: 左へ移動し、マーク
R
を探します。R
を#
に戻し、右へ移動します。テープは_111#11#
となります。ヘッドはテープの末尾の#
にいます。 - メインルーチンへの復帰と停止:
#
を読み込み、右へ移動します。テープは_111#11#_
となり、ヘッドは右端の_
にいます。ここで、これ以上処理すべき単項表現がないと判断し、停止状態へ遷移します。- シミュレーターでは: 状態遷移とテープ変化を追跡し、最終的に停止状態に到達する様子を確認できます。
このように、インクリメントという比較的独立したタスクをサブルーチンとして設計し、メインルーチンから呼び出すことで、全体の状態遷移をより整理して考えることができます。テープ上のマーク R
が、プログラミングにおける戻りアドレスのように機能していることが理解できます。
まとめ
チューリングマシンにおけるサブルーチンの概念は、状態のグループ化とテープ上でのデータの受け渡しや戻り先情報の記憶によって実現されます。複雑な計算を行うチューリングマシンを設計する際に、タスクをより小さな部品に分割し、それぞれをサブルーチンとして実装するという考え方は非常に有効です。
サブルーチンの設計においては、特に複数の呼び出し元から正しく戻るためのメカニズムが重要になります。テープ上に一時的なマークや情報を書き込む方法は、この問題を解決する古典的なテクニックです。
本記事で解説した単項表現のインクリメントサブルーチンの例のように、シミュレーターを活用することで、抽象的なサブルーチンの概念が、具体的な状態遷移やテープ上の記号の変化としてどのように実現されるかを視覚的に追体験することができます。これは、チューリングマシンによる複雑なアルゴリズムの実装を理解する上で、非常に役立つでしょう。
このサブルーチンの考え方を応用することで、より複雑なテープ操作や計算を行うチューリングマシンを、段階的に、かつ構造的に設計することが可能になります。ぜひ、ご自身で様々なサブルーチンを持つチューリングマシンを設計し、シミュレーターでその動作を確認してみてください。