プログラムとは、入力を受け取り、条件に応じて繰り返し処理を行い、最終的に出力を得る手続きの連なりである。FORTHでは、その繰り返しの仕組みさえも、シンプルなワードの組み合わせとして実現されている。ここでは、FORTHにおける主要な繰り返し構造――DO LOOP、+LOOP、BEGIN UNTIL、BEGIN WHILE REPEAT、そして再帰的ループ――について詳しく解説する。FORTHのループは単なる構文ではなく、「制御をデータとして扱う」思想の結晶である。
FORTHのループ哲学
FORTHでは、制御構造(条件分岐やループ)さえも「ワード(word)」として定義されている。つまり、C言語のような構文規則ではなく、辞書に登録された命令の連鎖としてループが構成される。たとえば DO も LOOP も単なるワードであり、内部的にはスタック上の値とコンパイル時のジャンプアドレスを操作して制御を実現している。
この設計思想の本質は、「プログラマ自身が新しい制御構文を作れる」という自由さにある。言語の仕様に閉じた固定的な文法ではなく、動作の定義そのものをユーザーが再構築できるのだ。つまりFORTHにおける「繰り返し」は、ハードウェアレベルの制御と、抽象的な思考の中間に位置する表現形式と言える。
DO ... LOOP
最も基本的で多用される繰り返し構造が、DO LOOP である。この形式は「指定回数だけ繰り返す」もので、C言語の for (i = start; i < end; i++) に相当する。
構文
DO ... LOOP
または増分を指定したい場合:
DO ... +LOOP
: COUNT-UP ( -- )
10 0 DO
I .
LOOP ;
0 1 2 3 4 5 6 7 8 9
この構文の仕組みを理解するために、まずスタックの動作を確認してみよう。DO の前にスタックには「開始値」と「終了値」が積まれている。先に終了値、後に開始値を積む。
( limit start -- )
FORTHでは DO が実行されると、ループカウンタ(開始値)が内部の制御スタックに移される。以後、I ワードで現在のカウンタ値を参照できる。LOOP はカウンタを1増やし、終了値に達するとループを抜ける。
+LOOP
+LOOP は、LOOP の「+1」を任意の増分に置き換えたものである。増加でも減少でも使える点が特徴だ。
例:
: COUNT-BY-TWO ( -- )
10 0 DO
I .
2 +LOOP ;
0 2 4 6 8
ここではループごとに I が2ずつ増える。
終了条件は「終了値を超えたとき」にループを抜ける仕組みになっているため、負方向にも対応する。
例:逆順カウント
: COUNT-DOWN ( -- )
0 10 DO
I .
-2 +LOOP ;
出力:
10 8 6 4 2 0
+LOOP を使うと柔軟な範囲指定が可能になり、DO LOOP の汎用性が一段と高まる。
I, J, K ― ループ変数の参照
FORTHでは、ループ中で現在のカウンタを取得するワードとして I が用意されている。
ネストしたループの内側で、外側のカウンタにアクセスしたい場合は J、さらに外側は K を使う。
: MULTI-LOOP ( -- )
3 1 DO
30 10 DO
CR I . J .
10 +LOOP
LOOP ;
10 1
20 1
10 2
20 2
: MULTI-LOOP ( -- )
3 1 DO
30 10 DO
300 100 DO
CR I . J . K .
100 +LOOP
10 +LOOP
LOOP ;
100 10 1
200 10 1
100 20 1
200 20 1
100 10 2
200 10 2
100 20 2
200 20 2
ループが入れ子になっている場合、カウンターは内側から I, J, K が使われる。FORTHの標準仕様(ANS Forth / Forth 2012)では I, J, K までしか定義されていない。
ただし、入れ子のループが発生した時にカウンターが移行する。つまり、最初のループでカウンターはIだが、次のループが発生すると外側のカウンターはJになる。
: MULTI-LOOP ( -- )
3 1 DO
CR ." Outer-I: " I . \ 外側のループのカウンターは I
13 11 DO
CR ." Inner-I: " I . \ Iは内側のループのカウンターになる
." : Inner-J: " J . \ 外側のループのカウンターは J に変わる
LOOP
LOOP
;
multi-loop
Outer-I: 1
Inner-I: 11 : Inner-J: 1
Inner-I: 12 : Inner-J: 1
Outer-I: 2
Inner-I: 11 : Inner-J: 2
Inner-I: 12 : Inner-J: 2 ok
LEAVE と UNLOOP
ループを途中で抜けたい場合には LEAVE を使う。LEAVE はその瞬間にループを終了し、次の命令(THENなど)へジャンプする。
: FIND-FIRST-EVEN ( -- )
10 0 DO
I 2 MOD 0= IF
." First even: " I . LEAVE \ → ループを終了する
THEN
LOOP ;
この例では、最初に偶数が見つかった時点で LEAVE によりループを抜ける。
ネストしたループの途中でループを抜け、ワード自体も終了する場合は、 UNLOOP を使う。複雑なループの中で異常終了する際には UNLOOP EXIT の組み合わせが定番である。
: FIND-FIRST-EVEN ( -- )
10 0 DO
I 2 MOD 0= IF
." First even: " I UNLOOP EXIT \ → ワードに定義された処理を中断する
THEN
LOOP
." Finish"
;
FIND-FIRST-EVEN \ → First even (Finishは表示されない)
BEGIN ... UNTIL
BEGIN UNTIL は、条件を最後に判定する後判定型ループである。少なくとも1回は実行される点が特徴で、「この処理を行い続けろ、条件が真になったら抜けろ」という表現になる。
BEGIN
(処理内容)
(条件) UNTIL
スタックのトップの値が真(−1)ならループ終了、偽(0)なら再度 BEGIN に戻る。
: WAIT-ZERO ( -- )
PAD 80 ERASE \ 入力領域初期化
BEGIN
CR ." Enter number: "
PAD 80 ACCEPT \ 入力文字列をPADに格納( addr len -- actual-len )
PAD SWAP \ スタックに ( addr len )
EVALUATE
0= \ 0以外なら入力を繰り返す
UNTIL
." Done" ;
0が入力されるまで繰り返し処理を行うループである。BEGIN〜UNTIL の組み合わせは「後判定ループ」であり、少なくとも1回は必ず実行される。
スタックの変化を追うと理解しやすい。
| 手順 | スタック内容 | 動作 |
|---|---|---|
| 初期 | [5] |
入力値 |
| DUP . |
[5 5] → [5]
|
5を表示 |
| 1- | [4] |
1減らす |
| DUP 0= |
[4 4] → [4 0]
|
偽(0)なのでループ継続 |
| … | … | |
| 最後 |
[0] → [-1]
|
真(−1)となり終了、DROPで消去 |
BEGIN ... WHILE ... REPEAT
BEGIN WHILE REPEAT は条件を途中で判定する「前判定型」ループである。C言語の while (条件) に近い。
BEGIN
(条件) WHILE
(処理内容)
REPEAT
条件が偽(0)なら WHILE の時点でループを抜ける。
: COUNTDOWN2 ( n -- )
BEGIN
DUP 0>
WHILE
DUP . 1-
REPEAT
DROP ;
5 COUNTDOWN2 → 5 4 3 2 1
この形式は、条件を先にチェックして「実行するか否か」を決める場合に適している。また、条件式をスタック上で構築できるため、複数条件を論理ワードで組み合わせることも容易である。
再帰による繰り返し ― RECURSE
FORTHのもう一つの強力な繰り返し手法が 再帰(recursion) である。定義中のワードを、自身の中で再び呼び出すことができる。このときに使うワードが RECURSE である。
: FACTORIAL ( n -- n! )
DUP 1 > IF
DUP 1- RECURSE *
THEN ;
4 FACTORIAL → 24
RECURSE は「定義中のワード名」を書かなくても自己参照できるため、ワード名の変更にも強く、柔軟な再帰処理が書ける。
: RECURSIVE-COUNT ( n -- )
DUP 0> IF
DUP . 1- RECURSE
THEN ;
5 RECURSIVE-COUNT → 5 4 3 2 1
このように、ループ構文を一切使わずに繰り返しを表現できる。ただし、FORTHではスタックが有限であるため、深い再帰は避けるのが一般的である。再帰は特に再帰的定義(木構造・探索アルゴリズム)などで威力を発揮する。
FORTHにおける「ループを考える脳」
FORTHでは、繰り返しの仕組みを “動作そのもの”として理解する ことが重要である。たとえば DO LOOP のような固定回数ループは、実際には次のような動作の連鎖でできている:
- スタック上に開始値と終了値を置く
-
DOにより、それらを制御スタックに移す -
Iがその時点のカウンタを返す -
LOOPに到達すると、制御スタックのカウンタを1加算 - 終了値を超えたら
LOOPの後へジャンプ
つまり、FORTHのループは「制御構文」ではなく「制御用スタックの操作」である。このため、プログラマは制御フローを単なる文法としてではなく、 スタックの状態変化として視覚的に理解 する必要がある。
同様に、BEGIN UNTIL や BEGIN WHILE REPEAT もすべて「スタックの真偽値を消費する操作」として成り立っている。条件が真なら戻らず、偽ならジャンプ――すべてがスタック上の数値で完結しているのだ。
この“スタック思考”を身につけると、FORTHのプログラミングは単なるコーディング作業から、まるで数式を直接操るような動的な論理設計 へと変わる。
応用:DO LOOP と条件分岐の組み合わせ
FORTHのループは、条件分岐と組み合わせることで強力な表現力を得る。
: EVEN-LIST ( -- )
21 1 DO
I 2 MOD 0= IF
I .
THEN
LOOP ;
2 4 6 8 10 12 14 16 18 20
FORTHでは、条件式とループ構造が完全にワードとして分離されているため、このような「演算+制御の合成」が極めて自然に書ける。FORTHを深く理解するとは、この「動作の積み重ね」を頭の中で可視化することである。
DO LOOP の内部動作を追う
gforth では .S コマンドでデータスタックの状態を観察できる。以下の例で、ループの挙動を観察してみよう。
: DEMO
3 0 DO
I .S CR
LOOP ;
出力例(途中経過):
<1> 0
<2> 0 1
<3> 0 1 2
各ループでスタックの状態が確認できる。I はループ変数をスタックにプッシュし、その都度 . や .S で表示できる。FORTHは、動作の可視化が極めて容易な「実験言語」なのである。
ループと時間・デバイス制御
FORTHが組込みシステムで重用された理由の一つは、ループ構造がハードウェア制御に極めて適しているからである。センサー読み取り、LED点滅、モーター制御など、時間や状態の繰り返し処理を簡潔に表現できる。
: BLINK ( n -- )
0 DO
LED-ON 500 MS
LED-OFF 500 MS
LOOP ;
このように、DO LOOP はハードウェア制御の基本パターンとしてよく利用される。しかも +LOOP を使えば、周期を動的に変化させるような処理も簡単に書ける。