レッスン13

ループ


【以下【】の中は訳者が付け足したものです。】

コンピュータープログラムには、しばしば、特定の演算操作を一定回数繰り返すことが必要となります。

例えば、スタック上の10個の数の和を見つけることは、普通は、引き続く9個のステートメントが用いられるでしょう。 プログラマーの考え方に対して見れば、これはプログラムを何段階か必要以上に長くしてしまいます。同じステートメントを長々と続けてプログラムのサイズを大きくしてしまうようなことなしに、加法演算を作業に必要なだけの回数繰り返す簡便な方法があればいいところです。 ここで登場するのがloop構成です。

ループ(loop【環路?】)はあなたのプログラム内に、初めと終わりのある一種のメリーゴーランドを設定します。 ループの終わりには、そのループの始めに"巻き戻る(loop back)"ようにとプログラムに告げる命令があります。 出発点から終わりまでの全てのステートメントが、プログラムの実行がループ内を通る度毎に繰り返されます。

Mopsは二つの主要なループのカテゴリーをもっています:確定ループと不定ループです。 その名前が示唆するように、各カテゴリーは、ループの循環をいつやめるのかを判定する方法が異なります。 確定ループはプログラムが特定した回数しか繰り返されません。;他方、不定ループは一定の条件に適うまで(いつまでも)繰り返されます。

各ループをもうすこし詳しく見てみましょう。

確定ループ

上で述べた10個の数の加法を考えましょう。足し算が起る前にスタック上に正確に十個の数値があることが予めわかっているのですから、スタック上に9個の足し算を実行する確定ループを使うことができるでしょう。

Mopsの確定ループは、DO...LOOPステートメントから成ります。これはDOが実行される前にスタック上に二つの数値があることを期待します。その二つの数値はそのDO...LOOPステートメントが為すべき繰り返しの回数を表現します。;二番目の値(スタックの一番上)は、ループが始まる前の【ループ指標の】増分を示します。

DO ... LOOP ( n1 n2 -- ) ‘n2’ はDO と LOOPの間の演算操作を実行された後、毎回増加します。; ‘n2’が‘n1’に等しくなったならループを抜けます。

ループはコンパイルされたステートメントの中でしか働かないので、その動作の仕方を見るにはコロン定義の中に置く必要があります。 足し算を9回繰り返し実行することによってスタックの10個の数値を加算する新しいワードを定義しましょう。:
: ADDTEN  ( n1 ... n10 -- sum )
   9 0 DO + LOOP . cr  ;
実行の間、このDO...LOOPは、ループの各回ごとに0から9まで数え上げます。9回目の巡回でループは止まります。; スタックの頂上(和)が表示され、改行が実行されます。

パラメタースタックが足し算される数の全体を保管するのに用いられているのなら、Mopsはどこでループカウンターをきちんと処理しているのかと不思議に思うかもしれません。その答えは、指標付け(indexing)と呼ばれる強力な機能に関連します。これは、Mopsについて学べば学ぶほど、重要な役割を担うものとなってくるでしょう。

上の例でDO...LOOP構成の前に9と0を入力したとき、これら二つの数はあなたの知らないうちにメモリーの別の部分に自動的に移されるのです。あなたが打ち込んだ最初の数(9)は、limit(境界)と呼ばれます。というのは、この数はそのループの内容が実行されるべき回数の境界値を表しているからです。

二番目の数(0)は指標(index)と呼ばれます。この数値は、ループを通った各回毎に1だけ増やされます。ですから、上の例で最初にDO...LOOP構成を通過したときには、指標は1に増えています。;次の回には2、等々、となるわけです。(LOOPにおいて)指標が増加される度毎に、指標と境界の値が等しいかどうかチェックが行われます。 もし等しければ、DO...LOOP構成は、いまや先に進むべきときであり、再度の繰り返しのために(DOへと)巻き戻されるべきではない、ということを知ります。

この種の指標付けの面白いところは、ループの実行中に指標値をカウンターとして利用できることです。境界値と指標値をあなたがループ内で演算操作するために必要な整数とすることによって、ループをまわる各回ごとに指標値をパラメタースタック上にコピーし、その数値を、計算や、図像を配置する点、かけ算の因数などに利用することができるのです。

指標値をパラメタースタックにコピーするMopsワードは"I"【英文字のアイ】です。:

I ( -- n ) 現時点の指標値をパラメタースタックにコピーします。
このワードは指標値をコピーするだけであることに注意してください。; 指標値そのものに影響を与えることはありません。いくつかの例証をみてみましょう。

ワードFIVECOUNTを定義します。これは101 から 105 までの続き数値を表示します。:
: FIVECOUNT 106 101 DO i . LOOP cr ;

境界は106に設定されていることに注意してください。指標は実行がLOOPに到達したときに増やされることに気をつけてください。初回の間は指標は101だったので、ワードIはその指標をパラメタースタックにコピーしました。; . コマンドが続いてそれをスクリーン上に表示しました。5回目の実行のときは、指標は105でした。実行がLOOPに至ったとき、指標は106に増やされました。この時点でいまや指標は境界値に等しくなったので、実行はこのループを抜け出したのです。

同じように、指標値は、実行前にパラメタースタック上に渡された数値と演算を実行するのに利用することができます。次の定義を見てみましょう。:
: TIMESTABLES  ( n -- )
   13 1 DO dup i * . LOOP drop cr  ;
【「予めスタックで渡された」という本文に合わせて、スタックを使うコードにしました。原文では名前付き引数を用いています。】

‘5 timestables’と打ち込めば、1から12まで増えていく指標値にその都度5をかけて打ち出すループを12回実行します。

DO...LOOP構成の中に、前に述べた条件決定構造を含む、あらゆる種類のステートメントを置くことができます。

記述を簡潔にするためにDO...LOOP構成を使いたいけれども、LOOPが自動的に実行する1の加算とは異なる指標加算量がほしいということもあるでしょう(LOOPのときは1増やすことしかできません)。このような状況では、オプションのループ終了ワードである+LOOPを使うことができます。+LOOPの前に数値をおけば、それが何であっても、DO...LOOPが指標を調整するのに用いる加算量となります。 ループの指標を減らしていきたいときには、負の数値も用いることができます。

+LOOP ( n -- ) LOOPの代用ワード。ループの指標を‘n’だけ増やし、指標が境界に達していなければ一番近いDOまで実行を戻します。
カウントダウンを+LOOPを使って管理する方法を示しておきます。:
: COUNTDOWN
   1 10 DO i . cr -1 +LOOP
   ." Ignition...Liftoff!" cr   ; 
この場合においてはループは逆方向にカウントしますから、境界は1、指標は10です。 ループを通る各回ごとに、指標は-1の分減らされます。このプログラムは境界値1も印字することに注意してください。指標がカウントダウンされて境界値に等しくなったときには、ループは継続され、指標が境界マイナス1になるまで止まりません。これは指標が増えていっているときと状況が違います(後者では指標が境界に等しいところで止まります)。 これを考えるときに一番良い方法は、「境界」と「境界引く1」の間に"フェンス"があるのだ、と考えることです。 どちららの方向からでも指標値がそのフェンスを越えたときにループは止まるのです。 これは、指標の増大によってループの走行中に符号が変わるとき、つまり、負から正になるときでも成り立ちます。

【ちなみに+LOOPの場合、指標のステップを巡回毎に変化させることもできます。しかし、途中でステップの符号を変えること(指標値を増やしたり減らしたりすること)はできないようです。】

入れ子状ループ


一つより多いDO...LOOPが同時に走行することが必要になることもあります。IF...THEN構成の場合と同じように、DO...LOOPも互いに入れ子にすることができます。 気をつけるべきことは、一つの定義の中で各DOに対応するLOOP(または+LOOP)をそれぞれひとつ提供しなければならないということだけです。
: NESTEDLOOP
   1 10 DO
         ." Loop: " i . cr
         4 0 DO
              ." Nested LOOP: " i . cr
         LOOP
  -1 +LOOP cr  ;
NESTEDLOOPと打ち込んで、外側のループの各1巡回が終了するまでに、内側のループがどのように繰り返されるのかを観察してください。

入れ子になった内側のループで、外側のループにアクセスしたいときには、ちょうどIと同じように、外側のループの現在の指標をパラメタースタックにコピーするワードがあります。それは、Jです。

J ( -- n ) 入れ子になったDO...LOOP構成の中で、その一つ外側のループの現在の指標値をパラメタースタックにコピーします。
言い換えれば、Jは、ちょうど一つ外の現在のDO...LOOP構成の指標を見つけて、その数値をパラメタースタックにコピーするのです。
: NESTEDLOOP2
 1 10 DO
       ." Loop " i . cr
    4 0 DO
         ." Loop " i .
         ." within Loop " j . cr
    LOOP
 -1 +LOOP cr  ;
注意:もし内側のループを別定義としてファクターしたならばJを使うことはできません。 — 正しい値を得ることはできないでしょう。 Jは現在の定義内の入れ子のループでしか働きません。

ループの中断


状況によっては、通常の終了前にDO...LOOPを抜け出す必要があることもあり得ます — おそらく特別な事情が起ったためでしょう。この目的のためにはワードLEAVEが利用できます。
LEAVE ( -- ) 現在のループを直ちに脱出します。
ここに、適当に修正したカウントダウンの例をまた挙げておきます。
: COUNTDOWN2
 1 10 DO
     i . cr
     i 7 = IF ." Aborted!!" cr LEAVE THEN
 -1 +LOOP  ;

"Ignition...Liftoff!"メッセージは取り除かなければなりませんでした。というのは、 そうしないとカウントダウンが中止された後に、そのメッセージが現れてしまうからです。 (これは実際望ましい結果ではありません。) これをもっとうまく処理する方法は、少し後で説明します。

不定ループ

不定ループはもうひとつの種類のループでMopsプログラムにおいてしばしば利用されます。 その名が示唆するように、不定ループは一定の条件が存在するまで巡回し続けます。 一回だけまわるということも、条件が起るのを待ち続けて何千回もまわるということもあります(そして許されるなら無限に続きます)。 Mopsでは、その条件とはスタックのトップにフラグがあることです。

BEGIN ( -- ) 不定ループの先頭を標します。
UNTIL ( n -- ) "n"が非ゼロ(true)ならば不定ループを抜けます; そうでなければBEGINの直後に実行を戻します。

BEGIN...UNTILによる不定ループは次のように使うことができます。:
BEGIN xxx UNTIL
(複数の)演算xxxは、UNTILのためにTRUEフラグがスタック上に置かれるまで(確定的な終わりはなく)繰り返し実行されます。

この構成の便利な亜種としてワードNUNTILを用いるものがあります。:
NUNTIL ( n -- ) UNTILの代用ワード。"n"がゼロ(FALSE)のときに不定ループを抜けます; そうでない場合はBEGINの直後に実行を戻します。
前の例の形では、UNTILの代わりにこれを用います。
BEGIN xxx NUNTIL

前と同様、演算xxxは繰り返し実行されますが、今回は、FALSEフラグがおかれるまでループは止まりません。

BEGIN...UNTIL構成の使い方の例を挙げます。 下の場合には、不定ループはあなたがキーボードで小文字の‘a’を入力するのを待ちます。 KEYオペレーションは、あなたがキーを押すまで、プログラムを停止させます。 そして、キーが押されたなら、押された文字に同値な標準的なコード番号(ASCIIと呼ばれます。— あとで説明します。)をスタックに押し込みます。スタック上の数値が十進で97(小文字の‘a’に対応するASCIIコード番号です)であるときには、スタック上に-1 (TRUEフラグ)がおかれ、ループが終了します。 それ以外の場合はスタックにはFALSEフラグがおかれ、実行はループの始めに戻ります。
: BEGINTEST
 BEGIN  key 97 =  UNTIL
 ." Loop broken." cr  ;
さて、BEGINTESTと打ち込んで、キーボード上の色々な文字を押してみてください。 小文字の‘a’を押すまで、このブログラムは循環しつづけます。

実はBEGINは別種の不定ループの開始点にもなっています。 新しいワードは、次の通りです。:
REPEAT ( -- ) 実行を無条件にBEGINの直後に戻します。
WHILE ( n -- ) ‘n’が非ゼロ(TRUE)である限り、ループ内の実行を続けます。
これら全部で三つのワードを組み合わせたものは、BEGIN...WHILE...REPEATループと呼ばれます。 当たり前のようですが、次のように使われます。:
BEGIN xxx WHILE yyy REPEAT
このステートメントは、ループを経由する毎に毎回xxxを実行しますが、実行がWHILEに達したときに、非ゼロの数値(TRUE) がスタックのトップにある場合にのみyyyは実行されます。もしも、WHILEでのフラグがゼロならば、ループを抜け、yyyが再び実行されることはありません。

ここでも、WHILEの変種としてNWHILEがあります。これは、スタック上のフラグがゼロ(FALSE)の場合にのみ、ループを継続します。
NWHILE ( n -- ) WHILEの代用ワード。‘n’がゼロ(FALSE)である限りにおいてのみBEGIN...REPEATループ内の実行を継続します。
上の新しい構成の働きを示すように、BEGINTESTを変形したのが次の例です。:
: BEGINTEST2
 ." Type a lower-case letter 'a', please." cr
 BEGIN
   key 97 =
 NWHILE
   ." Wrong key!" cr
 REPEAT
." Thank you. Loop broken." cr  ;
上の例は、小文字の‘a’を打つようにというユーザーへのメッセージから始まります。 前のBEGINTESTとは違って、このバージョンでは、間違ったキーを押したときに、ユーザーにフィードバックを提供しています。 正しいキーを押した場合には、実行はNWHILE以降を続けてエラーメッセージをプリントすることはありません。 というのは、=がTRUEフラグをスタックに残す(ユーザーが押したキーのASCIIコードナンバーが97に等しかったことを示します)ので、実行はこのループから完全に抜けて定義の最後にある最後のメッセージの印字へと続くからです。

EXIT

必ずしもループに関連するものではないのですが、ここで非常に便利なオペレーションであるEXITに言及するのがよいでしょう。

EXIT ( -- ) 現在のワード(またはメソッド)の実行を終了します。
LEAVEと違い、EXITはそこの定義を完全に抜けます。 初めのBEGINTESTをEXITを用いて書き換えた例です:
: BEGINTEST
   BEGIN
     key 97 = IF EXIT THEN
     key 98 =
   UNTIL  ;
この定義は、あなたが‘a’ (ASCIIコードナンバー97)か‘b’(ASCIIコードナンバー98)を押すまで走り続けます。次のように書くこともできます。:
: BEGINTEST
  BEGIN
     key 97 = IF EXIT THEN
     key 98 = IF EXIT THEN
  AGAIN  ;
そうです。BEGINを使うループはもう一つあります。こっそり使ってしまいました。
AGAIN ( -- ) BEGINの直後に実行を戻します。
もちろん、BEGIN...AGAINループを書いたならば、このループはEXITのような 何か特別な方法で終わらせなければなりません。

EXITをDO...LOOP内に書くときには、もうひとつ、忘れずにしなければならないことがあります。 — Mopsは(他のForthでも同じですが)DO...LOOPを実行する際にはいくつかの追加的情報を保存しています。 DO...LOOPを何か通常と違う方法で(つまり、LOOP、+LOOPまたはLEAVEを経ずして)終了させる場合は、その情報を取り除かなければなりません。そのために用いられるワードはUNLOOPです。

UNLOOP ( -- ) DO...LOOPから脱出する場合にリターンスタックから全てのループ情報を安全に取り除きます。
これをまたカウントダウンで例示しましょう。:
: COUNTDOWN3
   1 10 DO
          i . cr
          i 7 = IF  ." Aborted!!" cr UNLOOP EXIT  THEN
   -1 +LOOP
   ." Ignition...Liftoff!" cr  ;
また"Ignition...Liftoff!"というメッセージを復活させることができたことにお気づきでしょう。しかし、ループがUNLOOPとEXITで中断されたときには、これらメッセージは全く素通りされるのです。

注意:ループをデザインするときに意図せずして無限ループをすべり込ませてしまう可能性があります。これは絶対に避けてください!不定ループのスタック演算を慎重にチェックして、あなた、または、あなたのプログラムがループを終了させることができるような条件が少なくとも一つはいつもあるようにしてください。 そうしないと、あなたのプログラムは"閉じ籠って"しまったように見え、キーボードからの入力にも無反応になる可能性があります。もしそうなってしまったら、Mopsアプリケーションを強制終了しなければならないでしょう。


チュートリアル目次へ
前へ < レッスン12 次へ > レッスン14



最終更新:2018年12月07日 20:53