同じパターンの処理を何度も繰り返したいことがあります。そのもっとも単純な方法がループです。いや、実際に一番単純なのは必要な回数だけその処理を繰り返し呼び出すことですが、何十回、何百回となると、そうもいきません。
Mopsは多様なループ機構を備えています。大きな分類としては、確定(definte)ループと不定(indefinite)ループがあります。確定ループは、インデックスを用いて、予めその範囲を確定しておきます。何事もなければ、インデックスは繰り返し毎に増減され、その値が指定された範囲内にある限度でループは繰り返されます。他方、不定ループはインデックスによる範囲指定がなく、繰り返しの度に何らかの条件を調べてループを抜けるべきかどうかを決定します。
ループ構造はコントロール構造ですから、直接インタープリタで試すことはできず、何らかのワードの定義に書き込まなければなりません。
Mopsのもつ様々なループの使い方については、
Mops細説A面を参照してください。確定ループと不定ループとして、3ページずつ説明してあります。ここでは、確定ループであるDO-LOOP系について実験を交えながら少し詳しく説明します。
確定ループ
上限を定めるループには、DO-LOOP系のワードセットと、FOR-NEXTワードセットがあります。FOR-NEXTセットは効率が良いと言われますが、付録扱い(オプショナル)のもので、Forth標準は、DO-LOOP系です。
この、限定回ループではループ回数を示す指標である「i」を用いることができます。ループ内でアルファベットのアイ「i」を書くと、その実行毎に、現在のループの指標(第何番ループか)を表す数値がスタック上におかれます。
また、限定回ループといえども、いつも決まった回数だけ繰り返さなければならないわけではありません。一定の条件が満たされたときには、臨時にループを抜けるためのワードも備わっています。
基本形
では、まず、DO-LOOPから説明してみます。もっとも単純な形式は
[境界値] [初期値] DO [コード] LOOP
というものです。"DO"は、初めに二つのスタック値を入力として取ります。右側(トップスタックアイテム)の[初期値]が開始点のインデックス値、左側(セカンドアイテム)の[境界値]がインデックスの境界値になります。これらは負の値でもかまいません。この形式の場合、インデックス値は1ずつ増えていきます。開始値と境界値で決まる回数だけ、"[コード]"は繰り返して実行されます。
実際の動作としては、まず入力値を格納したあと、"DO"の後ろから[コード]を実行し"LOOP"に至ったときインデックス値が1増やされ、境界値と比較されます。インデックス値が境界値より小さければ、再び"DO"の後に戻ってコードが実行されます。インデックス値が境界値以上であるときには、もう繰り返すことはなく、"LOOP"の後に実行が移っていきます。
この動作から、DO-LOOPで注意しなければならない特徴が二つわかります。一つ目は、原則として"[境界値] > [初期値]"であることが前提とされてはいるものの、この関係が成り立っていなくても、DO-LOOP内のコードは、最低一回は実行されてしまうということです。二つ目は、インデックス値は"[境界値]"までではなく、"[境界値]-1"までである、ということです。実演で試してみましょう。(^^)
Mopsで次のようなワードを定義します。
: KazoeAge ( n1 n2 -- ) DO cr i . LOOP ;
数字を順に縦に印字するだけのワードです。
数え上げコード
まず、PowerMopsをダブルクリックして起動するして、上のコードを書き込みましょう。行の最後でENTERキーを押してください。returnキー(改行)とは違いますから注しましょう。これで、コードはコンパイルされ、辞書に格納されます。なお、左の画像では、バージョン5.5を使っています。以前より起動時のウィンドウサイズが大きくなっています。もらってすぐは、ウェルカムメッセージが書かれていますが、要らないので消しましょう(^^;;)。
次に、実験です。まず入力値として、"5 10"を取ってみましょう。つまり、大小関係が逆転していた場合です。5 10 KazoeAgeと書いて、enterキーです。すると、結果は次のようになるはずです。
始まりのインデックスはトップアイテムで与えられる10ですが、境界値はそれよりも小さいわけで、初めから限界を超えているのだから、一回も実行されないという期待もあるかもしれません。ところが、DO-LOOPは前にも述べたように、まずコードを実行し、インデックス値を1増やした後、初めて境界値と比較します。この際には、11は当然5より大きいので、コードが繰り返し実行されることはありません。ですが、最初のインデックスが10のところは、実行されてしまうのです。これが、10がひとつプリントされる結果につながります。
では、入力値を"10 1"としてみましょう。すると、左のような結果になります。1から10まで、とか思いませんでしたか?残念ながら、1から9までしかプリントされません。これは、初めに述べたように、インデックス値が境界値と等しいか大きい状態になった時点でループは終了するので、インデックスは9で終わりなのです。これは、直観的でないような気がするかもしれませんが、この性質によって、DO-LOOPは、"[境界値] - [初期値]"回繰り返されるということがいえるわけです。インデックス値を中途半端な数字から始める場合の実行回数という意味では、逆にわかりやすくなっています。また、最初のインデックス値は"[初期値]"の値で与えられるのですから、それほどわかりにくくはないと思います。ここまでは...(^^;;)。
境界値はインデックスにならないこと
ヴァリエーション
DO-LOOPにはヴァリエーションがあります。細かくいうと、"DO"のヴァリエーション"?DO"と、"LOOP"のヴァリエーション"+LOOP"です。もっとも、気持ちとしては"+LOOP"が"LOOP"の原型なんだそうですが。
?DO
"?DO"は、入力値の大小関係にかかわらず1回は実行してしまうという性質を回避するものです。つまり、
[境界値] [初期値] ?DO [コード] LOOP
となった場合、[境界値] > [初期値]でない限り、"[コード]"は一回も実行されません。
試してみましょう。次のワードをPowerMopsで定義してください。
: ?KazoeAge ( n1 n2 -- ) ?DO cr i . LOOP ;
また、前と同じように"5 10"を入力として実行してみましょう。すると、下のように何も印字されないはずです。ちなみに、[初期値]=[境界値]でも実行されません。試してみてください。
今度は、ちゃんと印字する入力値ですが、どちらもマイナスの数です。結果は右のようになりました。マイナスの場合にも、[境界値]-1までというのは、正の数の場合と同じです。
+LOOP
続いて+LOOPですが、これはインデックス値のステップを指定できるようになります。具体的には、"+LOOP"の直前のトップスタック値がインデックスの増減値になります。このスタック値は"+LOOP"の入力になります。つまり。"+LOOP"はループの繰り返し毎にスタックアイテムを一つ消費します。
インデックス増減値とは、スタック値が正(プラス)の数なら増えていきますし、負(マイナス)の値なら減っていくということです。ただ、負の値のときには、ちょっと気をつけないといけないことがあります。ともあれ、まずは、正の値からやってみましょう。前と同じようなものですが、次のようなワードを定義してみましょう。
: Tobitobi ( n1 n2 -- ) DO cr i . 2 +LOOP ;
これはもちろん、印字される値が2ずつ増えていきます。
そこで、インデックスの始値を0で、境界値を21にして実行したのが下の図です。
実際、iが2ずつ増えていることがわかります。境界値は"LOOP"の場合と全く同じで、インデックス値が境界値より小さい限りは実行が繰り返され、同じか、またはインデックス値の方が大きくなったときに終了します。右の場合は、インデックス値が22になった時点で境界値21よりも大きくなったので繰り返しをやめています。もし、境界値を20にしていたなら、もちろん、18までしか印字されません。ぶっちゃけていえば、
: LOOP 1 +LOOP ;
ということに他なりません。
逓減インデックス
次に、インデックス値を減らしていく場合についてみましょう。すでに触れたように、"+LOOP"の直前のトップスタック値が負ならば、インデックスは初期値からその分ずつ減っていきます。ただ、インデックス値が減っていく際には、境界値との比較条件が反転されるということです。表面的には、当然といえば当然です。というのは、初期値が減っていくなら境界値は初期値よりも小さくなければならず、判定条件がもとのままなら、一回でループが終わってしまうからです。ここでの問題は、インデックス値が境界値と等しいとき、もう一回ループ内のコードを実行するかどうかです。インデックス値が増えていくときには、等しくなったらもう実行しません。これに対してインデックス値が小さくなっていくときには、等しいときにはもう一回実行するのです。規則風にいえば、インデックス値が次第に減少していく場合には、インデックス値が境界値と等しいかより大きい場合にはループ内のコードが実行され、インデックス値が境界値より小さくなったときにループは終了する、ということになります。インデックス値が増大していく場合と比べると、条件が完全に裏返っていることがわかると思います。実験してみましょう。次のワードを定義してみましょう。
: GyakuSo ( n1 n2 -- ) DO cr i . -1 +LOOP ;
インデックスが減少する場合 入力値を、"1 10"にして実行してみたのが次の図です。
10から1まで、11個の数字が印字されていることがわかります。インデックスが増大していくD0-LOOPに"10 1"という入力を与えた場合と比較すれば、その違いがハッキリするでしょう。減少する場合には、"境界値も含む"で実行されるのです。このてん、少し直観的でなくなる気がします。上のように判定条件の反転として考えれば論理的一貫線がないわけではない、とはいえますが。
ステップは可変
"+LOOP"は、折り返しの地点で、その都度スタック上の値を取ってその時点のインデックス値に加算するので、ステップはその都度違う値でもかまいません。"+LOOP"の直前に、ステップの値に当たる数値をトップスタックに置く適当なワードを呼び出してもいいわけです。
組み合わせ
"?DO"と"+LOOP"を組み合わせることも可能です。両方の効果が得られます。ただし、インデックス値が次第に減少していくようにした場合には、"?DO"と組み合わせることはできません。というのは、"?DO"はループに入るかどうかの判定を、いつも"[境界値] > [初期値]"であるかどうか、という条件に従って判定するからです。インデックス値が次第に減少していく場合、当然ながら"[境界値] < [初期値]"となりますから、ループが実行できないのです。
中途脱出
確定ループであっても、インデックス値が境界値に達してループが終了する以前にループを抜けることができます。それには、LEAVEを使います。普通は一定の条件が成就したときに実行されるようにIF-THENクローズと一緒に使います。これも試してみましょう。次のワードを定義してみましょう。
: Johgen ( n1 n2 -- )
DO cr i dup . 20 > IF ." wa Deka Sugi!" LEAVE THEN LOOP ;
なおメッセージはローマ字にしてありますが、日本語で打てないこともありません。OS Xではシステムの"言語環境設定"を開き、入力メニュータブを開いて、右下の"オプション ... "ボタンをクリックします。そのとき垂れてくるダイアログの"キーボードとテキストを一致させる" というところをチェックします。OS 9では、入力メソッド設定のメニュー(普通右上にあって、ことえりとかのアイコンがでるところ)から、"カスタマイズメニュー ..."というのを選ぶと、キーボード配列設定のダイアローグがでますが、その右下の"オプション ... "ボタンをクリックします 。そこで、表示されたウィンドウで、"フォントとキーボードを一致させる"というのをチェックします。あとは設定を閉じてもいいです。その後は、PowerMops上で日本語が表示できるようになります。フォントが変わったりしますが。
境界値が10なら普通 入力を"10 0"の場合は普通ですね。
境界値が20を越えると中止 ですが、入力を"30 0"にしてみると、右のようにループは21でメッセージ付きで止まります。
モジュール性
ループもまたひとつのモジュールとしてみることができます。インデックスの境界値と初期値は可変であって、外部から与えることもできますが、論理的にはこれらの値も込みでひとつのモジュールと考えるべきでしょう。どちらもループのための内部データだからです。確定ループは、これらの他にインデックス値という隠れた内部データを持っています。ですから、モジュールとしては
入力 |
[境界値] [初期値] DO |
|
i (インデックス値) |
|
( LEAVE ----------------> 脱出) |
出力 |
LOOP |
入力と出力はもちろんスタック上に置かれます。ですから、このモジュールは左側面がスタックに接触していると考えればよいでしょう。もちろん、入力も出力もなくてもかまいません。それらがない場合には、ループ内のコードは外部データを参照していることでしょう。その場合、その外部データとこのループは同じモジュールに属すると考えられます。
入力は直接的には、ループの第一巡目のコードに対して与えられます。初めの入力を全ての巡回で入力として利用したいときには、その都度コピーして使い、消費してしまわないようにします。ループを抜けたところで、不要ならば"DROP"で落とせば良いでしょう。ループと言っても、要は同じコードの繰り返しですから、初めの入力を第一巡目のコードが変形し、その結果を第二巡目のコードの入力にし、その変形結果を....というように、
データスタックを使って、次々にデータを手渡していくこともできます。
出力は、最後の巡回の実施後、あるいは脱出直前の巡回の際にスタックに残されたものということになります。
ループの実行中にも、
データスタックは自由に使うことができます。しかし、そのデータがループの内部で消費され出力として残らない限り、それは内部データでしかありません。このようなデータは、ループの外から操作することもできませんし、ループの外に影響を及ぼすこともありません。このような形で、ループはモジュールとしての性質を強く持つことができるようになります。
ループの入れ子、つまり、ループの中にループを置くことはもちろんできます。しかし、Forthの一般的傾向としては、複数のループが入り組んだものを一つのワードとして定義することは嫌われます。つまり、一つのループ毎に一つのワードを定義し、入れ子のループはループを含むワードの呼び出しで実現するということです。もちろん、絶対にそうしなければならないというわけではありません。しかし、そのように書いておけば、訂正、書き直し、保守が容易になるとはいえるでしょう。ループが明確に一つのモジュールとしての形を持っているわけですから、それが1ワードと対応するのは自然な発想といえるでしょう。
最終更新:2020年09月28日 00:28