プログラミング言語Forth 序章


Forthによるプログラミングは、ワードと呼ばれるサブルーチン(意味的に一定のまとまりのある処理の小部分、関数ともいう)を定義することで行われる。ワードの定義は、コロン記号で始め、次にワード名、そしてそのワードが行うべき処理内容の記述と続き、最後にセミコロンを置く。これが最も基本的なForthワード定義方法であり、コロン定義(:-definition)などとも呼ばれる。例えば、

: square-sum  ( a b -- a^2+b^2 )  dup * swap dup * + ;

このコードはsquare-sum という名前のワードを定義している。丸括弧の中はコメントで、実行には関わらない。
コード内の各ワードは、半角空白、タブ、改行、のどれかで区切られた文字列で表示される。: や ; もforthでいうワードなのであり、他のワードとの区切りに空白が必要である。

では、dup とか swap とは何だろう?
dup と swap は、スタック上の数値を操作するワードで、Forthのコアで定義されている。

では、スタックとは何か、どこにあるのか?
Forthは数値データの入出力にスタックというデータ構造を用いる。名前付きの変数は特別な場合しか使わない(といっても、かなりの頻度で利用するが)。スタックはグローバルに存在していて、いつもそこにあると考えて良い。コード内に数値(リテラル)を記述すれば、その値がスタックに積み込まれる。

dup と swap の内容は、スタックの説明の後に説明しよう。

データスタック

入出力を格納するスタックは、別の用途のスタックと区別する場合は、データスタックと呼ぶ。Forthにはスタックが最低2つ準備されている。もう一つはリターンスタックと呼ぶが、これもプログラマが利用できる。リターンスタックについては後で述べる。

スタックはLIFOリストなどともいわれる。LIFOはLast In First Outの略で、数値を取り出すとき、最後に追加した数値が最初に出てくるということである。つまり逆順に取り出される。コードに

5 4 3

と書けば、スタックの一番奥に5、一番表面に3が置かれる。使うときは、3 4 5の順で使われるわけである。

よくあるイメージとしては、リストを上下方向に延ばして"値を積む"などという言い方をする。例えば、本を平積みにして、取るときは最後にのせた一番上から順に取る、というわけだ。しかし、ソースコードは大抵横書きで書くだろうから、横方向で考えた方がわかりやすいかも知れない。つまり、値は、順に左から右に置かれていくのであって、使うときは右端から使っていく、ということである。

以下では、スタックは横向きに考えて説明する

スタックに値を「積む」のであるから、下から上に伸びると考えるのが良いという人もいる。
他方、実行コンソールに書き込むことを考えると、上から下に伸びると考えれば便利、という人もいる。
しかし、スタックの値の追跡が必要になるようなコードを書くときは、大抵、ファイルにコードを書いているときである。
ファイルにコードを書く際には、経験上、左から右に値を「置いて」行き、右端から喰っていく、と考えるのが、
自分には一番直観的な感じがする。

なお、老婆心だが、「逆順に取り出される」という言い方には注意を要する。いつも1個1個順番を考えるというわけではない。
例えば、"a b -"というコードがあったとき、「まずbを取り出し、次にaを取り出してbから引く」と考えてはならない。二項演算は、まず「上二つの数値」を取るのである。引き算[割り算]の演算は、前にある数値から[を]、後にある数値を[で]、引く[割る]と定義されている。したがって、"a b - " や " a b / "は、それぞれ、"a-b" "a/b"の演算を実施する。逆ではない。

定義内容

square-sumの内容は、要するに2乗和なのだが、ワードの定義を逐一解説してみる。
  • どのワードも、スタックの値は右から喰っていく。
  • dup は、一番右端の値を複製して2個にするのである。Duplicate(二重化)の略らしい。
  • swap は、2つの値の順序を入れ替える。スワップである。
  • * は掛け算、+ は足し算だが、2項演算は、スタックから2個の値を、まず取り除く。それから、その2箇の値について計算した値をスタックの右端に置くのである。

スタックが

7 8

のところに + を実行すれば、スタックは

15

と変わる。つまり、計算に使われた値は原則なくなるのである。
だから、dup は、1個値を使ってもまだ残しておきたいときなどにも利用される。
実行コードに関わらないスタックの奥の方は影響されない。使われない限り、ずっとそのまま保存されている。

中身を説明しよう。ワードの実行は逐次実行である。単純に書いた順に実行されていくと思って良い。原則、ではあるが。
スタックの初期状態を 3 4 として、スタックの変化を追っていくと、

3 4      dup
3 4 4    *
3 16     swap
16 3     dup
16 3 3   *
16 9     +
25

となる。この演算操作部分を1個にまとめたのがsquare-sumの定義なわけである。数値はスタックという無名変数を通じて抽象化され、実際の計算では自動的に管理されるため、ワード定義内での演算記述の中では見えなくなってしまうが、スタックの現在の状態を意識し判別しやすい処理手順を考えることが、良いforthコードを書く鍵ともなる。

ちなみに、forthでは伝統的に、英大文字と小文字を区別しない。dup もDUP も同じだし、dUpでも同じことである。わかりにくくならないように書けばよい。

スタックコメント

square-sumの定義には、ちょっと妙なコメントを付けたが、これはスタックコメントと呼ばれる。フルネームではスタック効果コメントともいう。そこで定義されているワードが、スタック上の値をいくつ使い、スタックに値をいくつ残すか、また、どのような種類(型)の値か、をコメントで明記しておくのである。基本は、

( 入力1 入力2 入力3 -- 出力1 出力2 )

のように、"--" で区切って書く。入力・出力それぞれに、右の値がスタックの上方(浅い方)である。
"(" はforthのワードである。つまり、コメントを書くときは、左括弧は他の文字列と半角空白で区切られていなければならない。右括弧はそういう条件はないが、区切って書く方がバランスが良いように思う。丸括弧によるコメントは一行コメントで、改行を含むことができない。また入れ子にすることもできない。そのため、コメント中に文字")"を含むことができない(コンパイラはそこでコメント終わりとして処理してしまう)。

Forthにおいては、型によって値が特別に細かく区別されることはない。整数か小数(浮動小数点数)かの違いしかない。整数であれば、数値でもアドレス(メモリーの場所を指す。多くはポインタとして用いられる)でも、同じようにスタックに格納される。これはコンピュータ機械自体の処理と一致している。しかし、プログラムのレベルでは値自体にはもちろん種類はあるのであるから、プログラマーは混同してはいけない。だから、コメントは値の型がわかるようなシンボルを使うと良い。上の例のコメントは、そういう点では、あまり好ましくない。

数値データを、変数名と同一視して名前によって保持・操作するのではなく、“揮発性の”値として用いるforthでは、スタックの状態の変化を見失う危険がある。スタックコメントで一番重要なのは、入出力の値の個数を明記することであるといえるだろう。
コメントであるから書かなくてもプログラム内容に影響はないが、デバッグや保守が困難となるだろう。

小括


コロン定義

上では、ワードsquare-sumが定義された。コロン : がワード定義の開始を告げる。その次の文字列(一文字でもよく、文字種の制限もない)は、ここで定義されるワードの名前を宣言する。そして、そのワードが実行されたときに行われるべきオペレーション系列、最後に、セミコロン ; でワード定義は終了する。このワード定義方法をコロン定義と呼ぶ。その一連の過程もまたコロン定義という。この方法で定義されたワードはコロン定義ワードなどという。このプロセスによって、新しいワードsquare-sumが、forthの辞書に登録されることになるのである。それ以後は、このワードは、その名前を書くことで「呼び出す」ことができるようになる。「呼び出さ」れた場合の動作は、コンパイル環境 — つまり、: と ; の間のコロン定義内 — では、呼び出しをコンパイルすることであり、実行環境では、実際に呼び出されて、その定義されたオペレーションがスタックに対して実行されることになる。

ワードの有効範囲

Forthのワードは、すべて、原則として、パブリックである。定義後はソースコードのどこからでも呼び出せる。けれども、他のある種の言語のようにヘッダファイルで定義関数を冒頭にまとめるという慣行はないので、あるワードが利用できるかどうかはソースコードのロードの順序に依存する。厳密な意味では大域 (Global)ではない。そのようなわけで「パブリック」という言い方をしてみた(公式の呼称ではない)。あるワードのコロン定義がロードされ、辞書にコンパイルされた後は、そのワードはどこからでも利用できるということである。
このように、ロードの時間的順序を除けばforthのワード定義は全く自由であるため、五月雨型の定義順序でするコーディングも可能である。
後で見たときに分かりやすいように整理して書くことは、プログラマーの責任である。

局所的にのみ有効なワードを定義するための特別な機構をオプションで持つことはできる。しかし、そのような特別なものを別としても、forthのワード定義は、ちょっとかわった特性を持っている。というのは、同じ名前のワードを再定義できるのである。そのときの挙動は、上書き定義というよりも切り替えである。つまり、同名ワードの再定義は、これまでの部分には全く影響を与えず、それ以後にロードされる部分では、新しい定義だけが有効になるのである。Forthではプログラムの意味の確定に関しても、時間次元が導入されているのである。

この特性は、forthワードに、いく分かの局所性、ないし、プライベート性を与える。モジュラーなコーディングを指向して、一つのまとまりの中で他のまとまりから呼び出されるワードを限定的に特定する、というようにすれば、モジュール内部的ワードはその場で定義されるであろうから、特に他との名前のダブりを気にする必要はないわけである。

この特性は、他方で、ソースコードを読む際の混乱を引き起しうる。また、形式論理主義(つまり時間次元の排除)の観点からすれば、
同一の名前には同一の意味が与えられていなければならない、と考えることになり、forthのやり方はこれと矛盾する。
しかし、工夫して用いれば便利な機能は、大抵、杜撰な使い方もできるものであり、そのときには有害なものになる。
また、特有の考え方さえおさえておけば、教条主義的になることに、あまり意味はないと思われる。


全てがワードの実行

Forthはセマンティカル言語とでもいうべき特性を持つ。Forthのプログラムにおいて重要なのは、行や文など長い構成体の持つ構文ではなく、各ワードの意味(オペレーショナル セマンティクス)である。各ワードの意味が語順に従って継続的に実現されていくことでプログラムは稼働する。

他の多くのプログラミング言語では、特殊記号を組み合わせて構成された陳述や文が読み取られ、それが分解分析され解釈された上でどのような処理が命じられているのかが判定される。その意味を確定するには決まった構文に従っていることが重要であり、その点からいえば構文(シンタクス)的言語である。自然言語(人が話し、読み書きする言葉)理論が、文法の形式的法則性に着目してそこから意味を割り出すというやり方で知識を蓄積していたこともあって、プログラミング言語理論はそれを利用して開発設計されたものが多い。結果として、言語理論というと構文の形式法則を操作するものしかあり得ないという誤信を招いたかのようである。Forthのような意味を中心にする言語を理論的に「武装」するには、まだ便利な方法がないようである。しかし、プログラミングが普及し、言語理論にあまりこだわらない発想で設計された新しい(といっても1980年代以降だが)言語は、forthにとは言わないまでも、Mopsと似たような構文を持つものも散見される。

Mopsは、Forthも元はそうであったのだが、実践において有用な機能を混同や混乱無く指定・記述できることを最優先した設計になっている。それは、いわば、データスタックを利用するプログラミングのためのUtilitiesの集合体である。言語はプログラマーに自由を与え、プログラマーが自らの能力と責任でそれを律していくという方針は正しいものと思われる。奇妙に教師然とした言語(とその威を借るプログラマー)が多く目につくけれども。

* 近年のforthには、標準化に向けた統制を強めようとしているようである。
 しかし、Forthにとっては普通のプログラミング言語のようになろうとすることが自殺行為なのである。
 ごく一部だが、標準化の議論を観察すると、forth言語規格の形で、forth開発環境のソフトウエアとしての仕様を規格化したいようである。
 コマンドラインからのコマンド入力であって、プログラミング「言語」ではないかのようである。
 この発想は、構造をもった言語の規格を考える上では、良くないように思われるのだが。

以下、forth言語について概説する。内部的な実装の話にも触れたい。当面は、基本的な既定義ワードの意味や語用法については、こちらを参照されたい。リンク先には、ややMops方言に傾くものの、基礎的なことを含めて大抵のことは書いたつもりである。ここに書くこととのダブりもあろうかと思われる。



最終更新:2019年11月18日 08:40