計算やデータ処理、あるいは外部関数の呼び出しなどを積み重ねていく場合に、スタック上の値の順番がいつも整合的に上手くいくとは限らない。また、同じ値を何度も使いたいときに、いちいち変数に格納するより、スタックアイテムを複製する方が簡便な場合もある。そのような必要に応えるために定義されているのがスタック操作ワードである。dupやswapは既に登場したが、経験上よく使うものをここで一括して説明する。その後、ワードを定義して、それをつなげていくことで行われるforthプログラミングについて、書法上の留意点のようなものを考えてみたい。

スタック操作子


ワード名とそのスタック効果は次の通り:
drop  ( x -- )   \ トップの値を捨てる。
dup   ( x -- x x ) \ トップの値を複製してアイテムを1つ増やす。
?dup  ( x -- 0 | x x ) \ トップの値が0でないなら、dupと同じ。0のときにはそのままで複製しない。
swap  ( x1 x2 -- x2 x1 )   \ 上2つの値の順番を入れ替える。
over  ( x1 x2 -- x1 x2 x1 )   \ ひとつ飛んだ2番目の値をコピーしてトップに追加する。
rot   ( x1 x2 x3 -- x2 x3 x1 )  \ 上3つの値を巡回、奥の値を上に出す。
down  ( x1 x2 x3 -- x3 x1 x2 )  \  rotの逆、上の値を奥にして2番目をトップに。-rot とも。rot rotと同義。*非標準ワード
nip    ( x1 x2  -- x2 )  \ ひとつ飛んだ2番目の値を捨てる。トップはひとつ下がる。swap dropと同義。
tuck  ( x1 x2  -- x2 x1 x2 ) \ トップの値を複製して上から三番目に挿入。上2つは、せり上がる。swap overと同義。
2dup   ( x1 x2 -- x1 x2 x1 x2 )  \ 上2つの値を複製し、その順のまま上乗せ。over overと同義。
2drop  ( x1 x2 -- )   \ drop dropと同義。
2over  ( x1 x2 x3 x4 -- x1 x2 x3 x4 x1 x2 ) \ 2つずつ塊で、下の対を複製して上に追加。
2swap    ( x1 x2 x3 x4  -- x3 x4 x1 x2 )  \ 2つずつ塊で上下入れ替え。
頭に2がついているワードは、文字列のように2つ対になって意味があるアイテムを操作する際には使うことがある。しかし、滅多なことでは使わないし、頻繁に使うようなら、ワードの定義の仕方を再考した方がよいかも知れない。

スタックの奥の方のアイテムにアクセスするためのforth標準ワードとして、PICKとROLLがある。
pick  ( xu ... x0 u --  xu ... x0 xu )   \ トップの値uを取って、その下のアイテムを0から数え始めてu番目にあたるアイテムを複製しトップへ。
roll  ( xu ... x0 u -- x(u-1) ... x0 xu ) \ トップの値uをとって、その下のu+1個のアイテムを巡回置換。一番奥を上に出す。
1 PICKはOVERと同値であり、2 ROLLはROTと同値である。u個飛ばした奥の値を取り出してくるのである。PICKは値をコピーしてくるだけなのでをそのまま上乗せ。ROLLは引っこ抜くのでダルマ落とし式にズラす。

Forthとスタック


Forthとスタックは対のようにいわれる。確かに、言語の機構と作動のメカニズムを支えている、というか、forthのような形の言語を可能にしているのは、スタックの働きが大きい。けれども、forthでなにかのコードを書くという観点から見ると、スタック、スタックと騒ぎ立てるのには違和感を感じるのである。

そのようなわけで、ここでは少し、スタックがforthでコードを書く際にもたらしている効果、について反省的に考えてみたい。それは結局、forthでコーディングするときの基本方針、というか、こう考えればいいんじゃね?的な話である。とはいえ、自分もforthでそれほどちゃんとコードを書いてきたわけでもない。なので、あまり役に立たないかもしれない。要するに、このページのネタはあまり書くことがないので、そういう話で埋めようかな、ということである。

CONCATENATIVE

Forthは最初のConcatenativeな言語である、とか、最近は、いわれる。要するに、ワードをズラズラと実行順に並べていく方式をいうのである。連鎖型というか、数珠つなぎ型というか。これは別にforthの構文規則というわけでもなかろう。
ただ、forthが、こういった構文で出力を入力へと受け渡し、関数を合成していける背景を作っているのは、確かに、データスタックである。
しかし、逆に、concatenativeのためにスタックのデータ構造が必要というわけでは全然ない。リストとか配列でもよい。スタックがよく使われるのは、
簡便で軽いからでしかないだろう。スタックというデータ構造は、コンピュータのデータ処理機構に、非常に良く適合したものなのである。
けれども、それは言語の仕組みとして、いわれてみればそうだろうという話であって、コードを書くときにはスタックのデータ構造を巧妙に用いてなどというのは、どうでもいい話のように思う。
Forthでのプログラミングにおいて、スタックは、喩えてみれば、人にとっての酸素や水のようなものである。確かにいつもそこにあるし、それがないと立ち行かない。
しかし、いつもそこにあるぞ〜とか意識したり気にしたりするようなものではない。その存在を意識させられるというのは、むしろ危機的状況にあることを示している。
そして、逆に、必要以上に浸ってしまうと、むしろ健康を害し、過剰になると生命さえ危ぶまれるのである。

連鎖型の言語と対比されるもの、要するに普通のプログラミング言語はほとんどなのだが、そこでは、関数の処理結果は、いちいち名前付きの変数に格納され、その変数を通じて次の関数の入力に明示的に渡されることで、手続がつながっていく。途中に変数を挟まないというところが、連鎖型の特徴、といえる。出力-入力というデータのつながりは、2つの処理間の意味の時間的依存関係を表す。ここで、意味というのはオペレーショナルセマンティクスということで、データ処理の内容のことである。その順序は時間的先後関係である:まず前の関数が働いて結果を出し、それを後の関数が利用するのであるから。この依存関係に留意すると2つの関連問題が派生してくる。ひとつはこの依存関係がスタックを経由して行われることからくる特徴、もうひとつは時間的依存関係という見方そのものからくる特徴である。

スタック経由の連鎖
スタックはいつも浅めに保つようにしないと、forthプログラミングは大抵破綻する。スタックは確かにデータを格納できるが、先行する処理がそこに溜め込んだ値を塊として次の処理に渡す、という考えは、forthに関していえば最悪である。複数の値を返すことはできるけれども、せいぜい2つ3つに止めるべきである。そして、機能的な意味の上でひとつにつながっている処理は、中断せずに、意味上のまとまりの、できる限り、最後まで続けて行うべきことになる。意味的まとまりの最後に成果として得られた値は、外部関数に渡されたり、どこかに格納されたり、不要なものは捨てられたりして、最終的にはスタックはクリアな状態になっているのが普通である。

そのようにスタックの圧力を感じながらコーディングすると、意味としてまとまっている部分はコードも一続きにまとめるという指向が出てくる。他方、依存関係のない独立な処理は相互に切り離すということになる。つまり、処理として独立のものの間で、あれを途中までやって、次にこれを途中までやってからまた戻り、みたいなコードは避けよう、ということである。そのような、コードの中であれこれ五月雨的に分散処理していくのは、大抵は避けるべきことだと思われる。コードが難読になる。しかし、変数経由の評価型言語のように、文面上の名詞の同一性で処理をつないでいく場合には、途中経過を保管する変数を準備しさえすれば、それぞれ独立の処理を、思いつくままの順で並べ、最後にまとめるのも、それほど困難ではない。Forthのように、コード面の地の部分、あるいは行間で処理をつないでいくやり方が、意味上の連結の力や相互の独立性を、より強く意識させる方向に働いているように思われる。
そのような独立性のあるまとまりなら実際の並行処理になじむわけであるから、コード内分散でなく、実際の分散処理に適すると思われる。

時間と論理構造
Forthの発明者であるCharles Mooreは、いちいち値を保管せずとも計算はできる、という考えはLISPから得たそうである。
変数を使わないとか、入れ子の括弧で関数を合成することをいっているのではないかと思われる。けれども、LISPはConcatenativeとはいわれていない。どこがちがうのだろうか。
おそらく、連鎖型言語では、処理が時間的に経過していくと考える点である。値の依存関係は処理時間の先後関係を拘束し、処理は前から順番に継起する、と。

当たり前ではないかと思うかも知れないが、論理的体系構造を想定し、証明を計算と考える、いわば数理論理主義的な発想からいえば、古典的に計算は無時間的である。というのは、論理的真理関係というのは、いつも既に真なのであって、計算を実施したかどうかには関係ないからである。関数も、入力と出力は同時に成り立っているのであり、その間の時間の隔たりは考えないのである。
例えば、リーマン予想のような数学の命題は、その証明ができるかどうかにかかわらず、もし真であれば、ずっと昔、宇宙開闢から真であったはずであるし、
未来永劫、真である。そのような経験的な知識に依存しないものを、哲学者のカントはアプリオリと呼んだ。数学はアプリオリである。

そう思って見ると、LISPの入れ子の括弧は、時間的継続を思わせない。空間的にはまり込んだパーツのように見える。実際、関数型言語の理屈は、上で述べた、古典的な数理論理主義的立場に立っているのではないかと思わせるふしがある。もちろん、実際にはデータ処理は手順を追って、時間に展開しなければ不可能である。けれども、関数型言語的発想では、少なくとも、コードが書いてある順序は処理の時間的順序を束縛しないのである。そう考えると、部分的にevaluateの順序を適当にずらすという発想は、無時間的なものである形式論理構造と、時間を使って計算しなければならない現実のデータ処理とのギャップがあってこそ出てくる発想(トリック)のように思えてくる。確かに、そういう観念が分ってしまった後ならば、特に関数型に限らず同種の機能は実装はできるのであろうが。

逆に言うと、forthのコードは、ロードと実行が一体として、自然時間に束縛されているのである。換言すれば、forthではプログラムのロード自体もプログラムの動作である。その意味で、ソースコードの書法の中にも時間的順序の観念が入り込むことになる。静的な体系を定めて、それに沿ってあれこれの結果を出す、というのではなく、動的なプロセスをそのまま書き込むのである。それは解釈実行という側面だけではなくて、定義とコンパイルによるシステムの成長という側面にも現れている。
例えば、Forthでは同名のワードを再定義すると、それ以前には影響を与えずに、再定義以後についてのみ切り替わるのが当然のように思われる。これに対して、LISP等では、再定義すると初めからその定義であったかのように動作するのが当然のように思われているようである。この違いは象徴的である。どちらが良いのかという罵り合いも見たことがあるが、これは優劣の問題ではなく、立場の違いの反映というべきなのだろう。

総体に、forthは動的な言語環境ということになる。それはコンパイル時にできることを実行時に後回しするという意味ではなく、プログラム自体が運動を生成しているということである。Forthは未来に向かって発展拡張する、反面、忘れられた過去は置き去りにする、という感じだろうか。

theoreinとpraxis

一般化すれば、大抵のプログラミング言語は計算理論などTheory(theorein:観照)を基盤とするのに対して、forthプログラミングは計算機オペレーション等のPractice(praxis:実践行為)を基盤としているといえるように思われる。上に述べた時間に関わる論点も、この見方に証拠を提供しているようでもある。これは、コンピュータ科学の根本的なあり方に関わるものなのかも知れないが、そこでは大抵、静止的な論理構造による理論が重要である。"教育の成果"であるのか、forthもそのように捉えようとする指向は強まっている。けれども、いつもどこかはみ出しているところがあるのである。一般のforthプログラマー(ベンダー)が通常主張するforthの利点も、theoreticalな見方からのものであり、私見では、そのような立場に立つ限り、開発環境としてのforthの利点は、もはやあまり無い。

Forthの指向


上の話の結論めいたことをまとめてみる。
ごく少数の値の受け渡しによるデータ処理の先後依存関係がforthでは意味上の強い結合関係として感じられる。他方そのような依存関係にない処理は独立した別系列をなすのが自然である。これは、全体の処理を比較的小さい意味のまとまりに細分して考える傾向につながるものと感じられる。
また、forthのプログラムは、観念的論理体系を静的に完結させるような記述ではなく、実際の処理手順を時間的順序にしたがって展開する、現実的処理の動的な記述である。ソースコードのロードの段階から時間的条件にしたがって発展していくのがforthであり、体系の完結のためにコードを書くことは無く、現実に実施される処理を必要なとき必要な順番でアクションの意味的連鎖として記述するのである。
意味上の小さいまとまりに分解する傾向は、concatenativeという特性から出てくるのではなく、やはりスタックを使うところからくるのである。
というのは、連鎖のデータ受け渡しのためのデータ構造が、サイズが不定だったり、どこにでもランダムにアクセス可能なリスト、配列、構造体など、
単なるデータの塊の一種なのであれば、まとまった一連の処理を小さく一回にまとめるというインセンティブは生じてこないように思われるからである。
その意味で、データの塊としてのオブジェクトという観念を導入することは、下手をするとforthの指向を破壊するものとなりうるのである。

ワード定義を小さく細かくまとめるという指向については、別のページで述べる。これも、データ処理の意味的まとまりという観点から分割するのである。




最終更新:2013年06月05日 16:47