簡単な大域変数の使い方にさえも、
データスタックの存在の影響は、陰に陽に構文に影響を与えていました。サブルーチンに当たるワードを自分で定義してプログラムを構成して行くとき、いつも常にそこにあるデータスタックを意識することなしに、Mopsプログラミングを適切に遂行することはできません。
ワードは必要な入力パラメターをスタックから消費し、処理後の出力をスタックに残します。複数個の出力を残すことも可能です。このやり方は、前のワードが残した出力値を次のワードが入力として利用し、さらにその出力を次のワードが利用し ... というワード間の直接的連続を可能にします。途中に値を媒介するための変数は必要ありませんし、変数が型付けられている言語のように、出力値と入力値に予定されている形式的な型を合わせるためにキャストする必要もありません。このことによって、非常に切り詰められた表記でのプログラミングが可能となります。
スタック操作のためのワード
上のような長所の一方で、スタックという機構の制約をかいくぐる必要がでてくる場合もあります。そのために、Mops/Forthは標準として、スタックアイテムを操作することを目的としたワードを規定しています。
デースタック上の値は、一旦演算処理等に供されたなら、そこで消費されなくなってしまうのが原則です。ところが、場合によっては、同じ値を二つ以上の処理に利用したり、一つの処理の中でも同じ値を水増しする必要がでてきます。そのためのワードが"DUP"です。これはDuplicate(複写する)の初めの3文字だそうです。つまり、このワードは、スタックの一番上にある値を、コピーして、その上に積みます。スタック上のアイテム数は1つ増えることになります。例えば、ある値を2乗するワード"POWER"を定義すると、
: Power DUP * ;
となります。" * "は、かけ算で、スタック上の二つのアイテムを使って、それらをかけ算した値をスタックに積みます。
こういう定義は、あまりに単純というか、素っ気なさ過ぎて却って分からない感じがするらしいですね。でもこれがForthなんです。
スタックアイテムは不可視化されてしまうので、何をしているのかわからなくなる場合もあり得ます。大抵の場合は、あるワードがスタックにどのように作用を及ぼすのかについて
コメントをつけることで事足ります。通常は、丸括弧を用いた一行コメントを使います。例えば、"DUP"に
スタック効果コメントを付けると、
DUP ( x -- x x )
です。かけ算の星マークなら、
* ( x1 x2 -- x )
といったところでしょうか。内容を説明して、後ろの方を"x1*x2"とか書いてもかまいません。コメントですから、どのように書いても、コンパイラやインタープリタには無視され、実行コードの内容にはなりません。丸括弧コメントは、始まりの左括弧は、両側が半角空白(かタブ)でなければなりません。まあ、左側は改行でもかまいませんが、一行コメントですので、右側で改行はできません。この左括弧自体が一つのワードなんですね。ワードを特定する名前は両側を空白文字で仕切られていなければなりません。隣とくっついていると、合わせて一つのワード名として認知されてしまうからです。そういう仕様です。コメントの見方は常識的にわかると思いますが、"--"の左側に書かれたシンボル個数分のスタックアイテムを使って、"--"の右側の個数分のアイテムをスタックに出力として残すわけです。各々の側で見ると、左にあるシンボル程、スタックの奥深く(下方)のアイテムを表しています。コメントには、そのワードにかかわるスタックアイテム部分だけを書きます。ワードが呼び出されるときのスタックの状態は様々でありうるわけで、実行時点のスタックの状態そのものを予見することなどはできませんから、当然といえば当然です。コメントで参照されないスタックアイテムにはそのワードは何の影響も与えないと考えればいいわけです。
ソースコードの中に既に以前に定義されているワードの参照のために書き込むなら、バックスラッシュ"\"を使って、行全体をコメントにします。これをつけると、それ以降改行までがコメントになります。このようにしないと、メモのつもりで書いたワードが実行されてしまいます。このバックスラッシュもワードですから、空白文字による区切りが必要です。
\ DUP ( x -- x x )
\ * ( x1 x2 -- x )
スタック効果コメントはとりあえず定義の際につけておくのがよいやり方です。後の"NIJOHWA"の定義を参照してください。
さて、場合によっては、要らないスタックアイテムを落としたいこともあります。それには"DROP"を使います。意味はわかりますね。
DROP ( x -- )
さらにデータスタックは中間的処理で得られた値を一時保管するのにも用いられます。ところが、"上から順にしか使わない"というスタックの性質のため、次に処理したい値が一時保管されている値の下になってしまったりすると厄介です。そこで、スタックアイテムの順番を入れ替えることが必要になります。それには"SWAP"を用います。
SWAP ( x1 x2 -- x2 x1 )
xとyからx2+y2を計算する"NIJOHWA"を定義してみましょう。
: NIJOHWA ( x1 x2 -- x ) DUP * SWAP DUP * + ;
まず、このワードのスタック効果コメントですが、上のように定義の際のワード名の直後におくのが一般的習慣のようです。
"+"はもちろん足し算で、スタックアイテムを二つ使って、それを足した値をスタックに積みます。
+ ( x1 x2 -- x )
ここでx=x1+x2ということです。
スタック操作も"NIJOHWA"ぐらいになると、初めての人は少し立ち止まるんじゃないでしょうか。アイテムを頭の中で、視覚化というか映像化して考えると良いんじゃないかと思いますが、慣れは必要ですね。詳しくコメントしてみると、
: NIJOHWA ( x1 x2 -- x )
DUP \ x1 x2 x2
* \ x1 x2*x2
SWAP \ x2*x2 x1
DUP \ x2*x2 x1 x1
* \ x2*x2 x1*x1
+ \ x2*x2+x1*x1
;
のようになります。足し算の順番が入れ替わってますが、まあ、結果には関係ありませんからね。
アイテムが2つまでの場合は、出そろいましたね。アイテムが3つの場合については、"ROT"があります。Rotate(巡回)の初めの三文字で、その名の通り、3つのアイテムを巡回置換します。
ROT ( x1 x2 x3 -- x2 x3 x1 )
コメントからわかる通り、"ROT"の巡回置換は一番奥のアイテムを一番上に引き出すという置換です。
ここまでにあげたワードがあれば、大抵の場合は、その組み合わせで間に合います。Mopsのコンパイラはこの点に関して最適化されているので、複数個連ねても生成されるコードに差は出ません。例えば、
ROT ROT
としても、素朴に"ROT"を二回実行するのではなく、
x1 x2 x3 -> x3 x1 x2
と一挙に変換するコードが生成されます。
ですが、複数個連続したスタック操作よりも、一個の簡明なワードのほうがわかり易いので、短縮型としていくつかのワードが定義されています。詳しくは、当サイトの
Mops細説A面の
スタック操作子(全部あげてあるわけではありませんが)や、Quick Edit(Mopsに付属のソースコードエディタ)に付属のMops Glossay(説明は英語で、アルファベット順ですが)などを参照してください。
スタックのもたらす効果
スタックアイテムには名前がないこと、そして一度使えば捨てられてしまうということ、これらの特徴は、プログラミングに思いがけない効果をもたらします。ごく抽象的なレベルの問題ですが、Forth系のプログラミングの考え方が、一般に普及しているプログラミング言語と異質に見える基本的な要因になっているように思われるので、書いておきます。モジュラー性の問題、つまり、まとまりのある各プロセスに独立性をもたせたいという要請に関ることです。ただし、Forth系といっても色々な変種があるので、特殊なものには下の議論は適用できないかもしれません。
一般的に、モジュール性は高い方が良いといわれます。これは、プログラム全体が複数個の小規模のモジュールの組み合わせとして出来上がっていること、そして、一つ一つのモジュールが独立であり、各々が一つの明確な機能目的を持つのが望ましい、ということです。プログラムの修正が特定のモジュールを取り替えるという最小限の変更で可能となること、機能単位でまとまっていることによりソースコードが読み易くなること、などからです。ここでモジュールというのは、データ処理のまとまりというごく抽象的な意味で捉えられます。実際に"モジュール"という名前の特殊な機構を提供している言語もある(実はMopsにもある)ので紛らわしいですが、ここで意図している意味としては、例えば、Mops/Forthではワードがモジュールですし、C系では関数、FORTRAN/BASICでは(サブ)ルーチンがモジュールです。オブジェクト指向では、オブジェクト(メソッドを合わせて考える)がモジュールでしょう。モジュールは通常、もっとも機械の処理に近い低レベルなものから、段階的な機能の観念的抽象化をへて部分集合風にまとめられて行き、最終的にハイレベルのアプリケーションへと至ります。アプリケーションもまた、一つのモジュールといってよいでしょう。
まず変数を宣言し、それを用いて値の遣り取りを行う普通のプログラミング言語では、一つの関数の範囲内だけで有効な局所変数であったとしても、それは一時的であれ、独立したストレージ(貯蔵庫)として現れます。変数は、その有効期間中は値を保持し、あるいは、新たな値を格納することが許されます。変数の転用は多分悪いプログラミング方法と考えられていると思います。実際上は型で縛るなどの方法が結果として変数の過剰な転用を抑えるように働いています。
値を伝達するにはほとんどの場合それを保持する変数が必要である一方で、一つの変数はその用途が既に完了した後であっても別の用途に重複して用いてはならないというのであれば、当然、変数を数多く宣言して用いることになります。局所変数は制限された範囲内でしか有効ではありませんから、それをどのように用いようとも、形式的な意味ではモジュール性を害するとはいえないかもしれません。しかし少なくとも、結果として、メモリー効率も実行効率も犠牲にすることになります。これもコードの読み易さや保守の容易さと天秤にかければとるに足りない費用と考えられるのでしょうか。それでも宣言されている変数があまりに多すぎるなら、それは一つの関数ないしサブルーチンで多くのことをしすぎていることを示唆するもので、さらなるサブルーチンに分割せよという信号となるでしょう。実際、局所変数宣言の長蛇の列が続くコードは極めて読みにくいものでしょうから。ただ、よほどのことがない限り問題視されないでしょうし、効率が気になるとしても、コンパイラの強力な最適化によって解決される場合もあるでしょう。そのため、名前付き変数に依存する文法形式は、本来はエンジニアになじみ深い数式を模倣するために導入されたものと思われますが、この局面では、どちらかといえば、やや弛緩したプログラミングを支援する結果に、図らずもなってしまうでしょう。例えば、『ここで一応値をとっておいて、この続きはもう少し後で...』(場合によっては、そのまま忘れる^^;;)のようなことが起こり易いのではないでしょうか。例えば、C言語で書かれたサンプルコードを見るときなどには、場合によってはエディターの変数名の検索機能とか活用しませんか?私だけ?関数の定義が長くなると、変数を目だけでは追跡できなくなることはあり得ると思いますが。
これに対して、データスタックのストレージとしての有効期限は、その値を利用するまで、です。1度使えば当アイテムは無効にされてしまいます。実に短いわけです。変数のイメージである、有効期間中は好きなときに値を格納して好きなときにそれを使い好きなときに中身を入れ替えられる箱、とは大きく性質が異なります。データスタックはどのワードを実行しているときもいつもそこにあって、ワード間の値の伝達にも利用できるという大域変数的な地位を占めているにもかかわらず、無名性と使用後即廃棄という性質によってむしろモジュールの緊密な結合と独立性を高めることになっています。ここでも少し話はそれますが、効率性の話をすれば、データスタックは初めからその領域は確保されているので、モジュール呼び出しの都度特別に領域を確保する必要はない上、アイテムは変数のように初期化しないで値を使ってしまうという危険がないため、いちいち初期化の必要もありません。また使用済みの領域は当然のように再利用されます。したがって、メモリー効率はもちろん、ワードを呼びだす際の実行速度の点でも、多くの言語(特にCを考えていますが)の普通のサブルーチンコールより処理は軽くなり、速くなる可能性があります。つまり、効率も良い上に、よいプログラミング習慣とされている緊密なモジュール化を相当に強く要請する効果があるわけです。
Mops/Forthに関して平たくいえば、ワード定義内でのワード間の結合が緊密にならざるを得ないということを意味します。確かに、スタックアイテムも、値を貯めておいて何度も使うことはできますが、その場合には、明示的に値をコピーしなければなりません(変数は黙示的に値をコピーして使います)。特定のスタックアイテムに個性をもたせ、それを値であるかのように観念することはありません。スタックアイテムは局所変数と比べてより迅速に値を使い切るためにできています。変数を"箱"とするならば、スタックはちょっと値をおいておく"棚"という感じでしょうか。こういった性質を通じて、データスタックは、弛緩したプログラムを書くことを避けるようプログラマに心理的・技術的な圧迫を与えます。名前付きの変数があればそれほど困難ではない類の、中間的な値を残しつつ、五月雨的にいくつかの処理を併行実施しながら最後に統合するというような処理を一つのワードの中で実行するのは、不可能ではありませんが、非常にやりにくくなり、効率も悪くなります。スタックが深くなったり、長い間値をスタックに格納したままにしたりすると、何がどこにあるのか見失いがちになります。スタックアイテムの状態は、特別にコメントしない限りソースには現れないのです。しかも、奥の方の値が必要になって取り出しのために順番を入れ替えたりすると、スタックの状態は極めて把握しにくくなります。頭の中にある処理のロジックに従ってソースコードを書いている段階では大きな問題はないかもしれません。しかし、読む方は大変です。つまり、データスタックを用いるプログラミングは、その性質上、弛緩したモジュールを嫌うのです。しかもそれは、文法規則によって禁止するのではありません。限界を超えると一挙にソースが判読不能へと崩落していくという、プログラマに対する手痛いしっぺ返しを喰らわせることによってその意志を表明するのです。
この経験をどの方向で捉えるのかで、Forth系言語に対する態度が真っ向から対立することになります。一方の人は、この要請から、緊密にモジュールを結合し機能単位を小さく分けてスタックアイテムを少数に保つプログラミング手順(問題解法)を習得し、この方法の習得が自身の他言語でのプログラミングにおいても有効に働いている(速くコンパクトなコードが書ける)ことを見いだしさえします。他方の人は、Forth、というよりデータスタックを用いるプログラミング言語一般は、コードが理解不能となるという致命的な欠陥を抱えるものであると断罪し、離れていくことになります。一般のプログラマからの言語に対する評価という点からは、データスタックもよいことづくめではないわけです。
むしろ、普通のプログラマの反応としては、データスタックを使うことには、致命的な欠陥がある反面特段のメリットはない、と考えるのが普通のようです。個人的には、"致命的欠陥"の本当の在処を問題にしたいところですが、慣れた方法と異質なものに適応する過程が人を苛立たせるものであることは共感できなくはありません。たまたま私のようにほとんど白紙の状態だとそれほど障害もなかったわけですが。もっとも、もちろん私の適応の程度など大したことはないのですが、こういう類の話は、自分のことは棚に上げて偉そうなことを書かないといけないので、心理的抵抗を感じつつ書いています。
多分、中立的な立場からいえば、変数を使うプログラミング言語では、ある程度以上に酷いコードを書くためにはむしろ技術が必要(意図的に酷くする必要がある)ですが、データスタックを使う言語では、コードの酷さは技術が足りないことの顕れでしかない、といったところでしょうか。C言語系は一般に酷いコードを書き易いといわれていますが、Forthはさらにその上をいくと言えるかもしれません。他方、実はForthはC言語系よりも、よいコードもまた書き易いのです。つまりForthは自由度が大きい。一般にそれをよいことと感じる人もあれば、悪いことと感じる人もいますよね、世の中には。みんなが自由になりたいと思っているわけじゃあない -- 口ではどう言うかは別として。
実際にはForth系の諸言語でも局所変数は利用できます。しかし、これも、データスタックの操作が複雑になりすぎないための補助手段として位置づけられていることに注意する必要があります。結局、スタックを指向しつつモジュールを構成していくのが原則なのです。
モジュール(ワード)が内部的に強く結合されていること、その結果としてモジュール(ワード)の単位区分が細分化される傾向にあること、こういった事柄が、データスタックを用いることによって要請されるのです。この圧力を感じて対処していける程度の柔軟性を欠いた場合、Forth系の言語によるプログラミングは不愉快きわまりないもののように感じられる可能性があります。Forthはシンタックス規則その他の禁止事項によって不適切なプログラミング習慣を排除するというようなことはしてくれません。練習しているうちに、何となく以前よりもコンパクトにコードが書けるようになった気がすると嬉しいものですが、裏からいえば、全く最初からきれいなコードをいつも書けるということは、その人が天才ででもない限り、起こりにくいことであるようです。Mopsでコードを書く際にも、まず第一次的には、Forth型のモジュール化を指向すべきであると私は思います。もっとも、Mopsには、Mac OS上の開発環境として、伝統的なForthとは異なる部分も多く、また多くの抜け道も準備されているのですが。
例えば、Mopsの場合、クラス- オブジェクトという、別系統のモジュール化機構も持っています。これはどちらかといえば、C言語系で書かれた、やや弛緩したモジュール(関数)を前提としてそれらを解体しない程度につなぎ止めるものといえるかもしれません。あるいは、途中の抽象化段階を飛ばして、素材を一挙に抽象的な観念でゆるゆると結びつけるというべきでしょうか。いずれにせよ、データスタックの要請する緊密なモジュールよりはかなり緩やかです(データ構造による結合となっているのが普通です)。もっとも、緊密さの程度似違いがあるのには理由があるわけです。オブジェクト指向は、そもそも極めて緩い結合が適当なモジュールを構成するのに便利であるために、おそらく普及したのです。C言語系の発想で書かれたOSの機構を利用する上で、Mopsのオブジェクト指向機能は非常に有効に働いていると思います。
ここで書いておきたかったのは、Forth系でデータスタックを用いるということが、ただ、そこにそのような使い方をするメモリ枠が準備されているというに止まらず、クラス-オブジェクト機構と並べて考えることもできる程の、非常に抽象度の高いレベルでプログラミングにとって意味を持っているという事実です。データスタックは、変数云々より、もっとハイレベルの問題なのです。C言語などでは、リターンスタックとデータスタックを合わせたような一種類のスタックをサブルーチンコールに利用しますが、その働きは非常に低レベルのものに止まるので、プログラミングの際に表面には出てこないように隠されています。Forthで顕在的スタックというときには、そのような、つまり他の言語でいうスタックと同じものが連想されて、かくも低レベルの素材が露出されているという理解になりがちです。しかし、そうではないのです。
ところで、Mopsはさらにこれに加えて非常に多様な側面も持っているわけです。私の個人的な感想ですが、Mopsについて一番驚いたことは、データスタックを用いることやあからさまに低レベルの操作を行う機構とオブジェクト指向実装とが双方から潰し合うことがなく両立している(少なくとも私には齟齬が見つけられない)ということです。なぜそのようなことが可能になったのかは、私にはまだよくわかりません。
ここまでおつきあいくださって、ありがとうごさいます。ちょっとややこしかったですかね。
最終更新:2019年01月02日 17:28