Forthプログラミングは難しい?


当サイトでは標準forthの欠点ばかり書いてきた気もするが、これはforth言語の優れた面の表れでもあるのだ。
弁解ではなく、本当に。

Forthでのプログラミングは、言語の提供している機能を利用する側面と、必要な機能を提供する言語要素を構築する側面がある。他のページにも書いたように、forthでのプログラミングは、初心者を一歩出た段階で言語実装へと立ち入ることになる。forthでいう拡張性はボキャブラリーが増えていくというだけに止まらず、プログラミングの仕方自体を変更するものとなっているのである。

コードや機能それ自体より、アイデアを

いわば一種のアプリケーションとして提供されている他言語のハイレベルな機能に相当する手段が、既にforthで提供されているのではないかと、ときおり話題になることがある。しかし、それは微妙にズレた見方のようにも思われる。forth言語は、直接にそのような機能を提供しているというよりも、そのような機能を実装するための方法というか原型を提供しているのである。大抵、上のような話題の結論は、forthには似たような機能はあるが、それほどハイレベルではない、というところに行き着く。しかし、実際には、そのようなハイレベルな機能をforthで実装することは可能であると思われる。他言語で提供されていてforthで実現できないハイレベル機能は存在しない。たまに、「forthでは○○ができない」とか、「forthの○○には××が欠けている」という言い方をする人がいる。しかし、それは、実際には、forthにできないというのではなく、それを実現する具体的な方法を、当の本人が理解できていないというだけのことなのである。その機能がどのような手順で実現できるかが分れば、それをforthで実装するのは驚くほど簡単であることが多い。

簡単 — あくまでも「相対的に」だが — というのならハイレベルな機能を初めから言語の機能として組み込んでしまえばよかったのに、とも確かにいえる。これをしなかったのは、もちろん初めはサイズが大きくなることを避けたのであろう。しかし、問題はそれだけではないのである。

ある傾向のハイレベルな機能がforth環境に定着し、それを当て込んだプログラミングが一般化するなら、その環境でのプログラミングのやり方自体が、「forth的」ではない方向にずれ込んでいく可能性が大きくなる。それを好い現象と考えるかどうかは判断の分かれる所であろう。少なくとも伝統的には、forthプログラミングの視点からすれば、そのような変移は一般には好ましくないことと見られてきたように思われる。というのは、そういったハイレベル機能を用いることでアルゴリズムないし処理手順の上で非常に有利になるアプリケーション問題もある反面、別のやり方の方がもっと簡潔な解が得られるという場面も多くあるはずである、と考えられるからである。けれども、ひとつ一般的な方向性を獲得してしまうと、何でもそれでやってしまおうとするようになるものである。対して、可能性はできるだけ捨てずにとっておく、というのがforthの流儀である(といっても、現在ではこの発想は風前の灯火であるのだが。)。

Forthのやり方とは、現実に具体的に直面しているアプリケーション問題ごとに、最も簡潔に処理できるような抽象化を考えて、それに対応するハイレベル機能を自分で構成するための必要充分な手段を与えよう、というのである。初めから観念的な概念ゲームにあまり関心はないが、現実的処理方法のアイディアには関心がある。やり方をそのまま真似するのではなく、アイディアを自分のやり方で試そうとするのである。一般に、言語設計者には、そのような問題の抽象の方向性を言語が提供する特性枠として与えておこうとする傾向があるように見える。けれども、それはつまり、その方向しか許さないということでもある。Forthでは、抽象的なままの問題を言語構造で解いてしまい、方向を言語仕様として決めてしまうことを、むしろ嫌う傾向がある。そのように制限しなくてもプログラマーは答えをだせると考えるのがforthである。(これが間違いであったかのようでもあるが。)

具体的な処理を書くだけならアセンブラで十分ではないかともいえる。確かにそうなのだが、アセンブラはあまりに具体的な機械に密着し過ぎているため処理が煩雑になり、汎用性もない。機械の個性を捨象できる程度にハイレベルな言語であって、しかし機械の具体的動作を制御する力はアセンブリ言語に匹敵できるようなもの、その辺りがforthの目指した所なのであろう。Forthに対して、抽象度を容易に上げていけるアセンブラのようなものという評価があるが、それほど外れていないと思われる。

他方、forthは自分の必要とするものを準備してくれていない、という類の非難は比較的良く見られる。この裏返しが、ある種のforth方言ではこんなにリッチな高級装置が云々、という評価であろう。要するにそういう発想は、普通のプログラミング言語ではもっともなのだが、forthに関しては頓珍漢な発想と見られてしまうのである。必要ならば自分で実装すればよいのである。そのためには、その機能をどうやって実現するのかを考える必要がある。しかし、アイディアがハッキリと分ってしまえば、実装できないことはないだろう。いくら美味い料理を食べ歩いても、それだけでは料理の腕は上がらない。他からアイディアを得て、どうすれば実現できるのか試してみることが重要ではなかろうか。もっとも、確かにforthは、単純なソートやリスト作成のアルゴリズムも考えられない人に対しては親切な言語とはいえないかもしれない。それは今日では敬遠される十分な根拠になるのであろう。

広い範囲で部分的変更ができるという特性

普通のコンパイル型の高級言語ならコンパイラーや静的リンカーがやるようなことも、forthでは、forth言語自体を使って自分で書くことができる。例えば、Mops(メタなんたらではなく、forth系言語名)では局所変数はレジスタ変数を優先的に用いるが、レジスタを保存-回復したり、値を初期化したりするコードも、forthで書いてあるのである。そのようなコードはプロローグ-エピローグコードとかいわれるが、局所変数定義用のワードを定義するときに、その中身として後から自分で付け足すことができるのである。また、MopsにはSYSCALLとか、:ENTRYとか、:CALLBACKといった、特殊な関数を定義するためのワードセットがある。これらも、使用される状況に応じて必要なプロローグとエピローグを、内容として定義されるコードに追加するのである。そうすることで、普通のコンパイラ言語ならコンパイルコマンドにオプションフラグとかを付けると追加されるようなデータやコードを、自分で内容を決めて任意に追加することができるのである。もちろん、forth言語を用いてアプリケーションと同じ水準で。

コンパイラーやリンカーがやるはずのことをなぜ自分がやるのか、という考えを持つのは、ここでの問題の意味を理解していない。確かに、決まったパターン通りにやるだけなら、自分でしなくても良い方が楽である。いや、汎用的なものならforthでも自動的にやってくれる(とはいえ、開発環境を作るときは、ここもforthで書くのだが。)。しかし、都合でちょっと特殊な変更を追加したいような場合はどうだろうか。そのようなときには、通常ならコンパイラーやリンカーを自分で書き直してビルドし直さないといけないのである。対してforthでは、追加ワードを定義するだけで、同じことが可能なのである。しかも、他の部分には全く影響なく!

メタオブジェクトプロトコル(MOP)を提唱していた文献では、そういう、ちょっと追加したいような部分は、プログラマー限りで追加できるような仕組みを予め付けておくべきだ、と言語実装者に勧めているようだった。しかしforthでは1970年代からそれはアプリケーションプログラミンングの普通の一部として可能だったのである。
実は、コンストラクタが何をするのかは普通はプログラマーが自分で決められないものらしいということが初めはわからず、
MOPの提唱が一体何がいいたいのか理解するまで少し時間を要した。
とはいえ、逆にforthではこれが無原則で広範に許され過ぎているともいえる。その点では、MOPという考え方が、論理的な階層として整理してくれたのは重要だと思われる。というのは、観察によれば、forthプログラマーでも本能的に上手く処理できていた人は存在していたようだが、一般にはそこに特殊な問題があることが気づかれていなかったことが酷いコードを産出する原因のひとつになっていたように思われるからである。そのような状況であったので、一般には「プログラマーにもっと強いパワーを与えよう」という話として受け取られたと思われるMOPは、forthから見れば、プログラマーが持っている強力なパワーを合理的に律するための観点を教えてくれるという、制限的なものとして有用なのである。確かにforthの場合でも新機構を追加しようとするときは、どのようなオプションをプログラマーに与えるかという観点から参考になる部分もないではないが、脇道を接続するのが難しいほど入組んだ実装方法に固執したもので限り、欲しい仕組みは自分で足してね、といえるのがforthなのである。
個人的には、オブジェクトシステムを実装する際にも、後で何か追加機能を接続するときにもややこしくならないよう、できるだけ単純にするよう
こころがけた。まあ、それは大抵、いちばん簡単な方法なのだが。

局所的変容の原則

上に、コンパイル動作の変更が「他の部分には全く影響なく」可能であることを強調した。このような特性、個人的には「局所性」などと呼んだりしているが、この特性がforthの著しい特徴であるように思われる。これは、別の言い方をすれば、本質からしてモジュラーであるということだ。

モジュラーの意味としては、第一に、各モジュールが機能ないし意味単位としてまとまったものとして捉えられるということが前提となる。その上で、一つには、モジュールの外(ないし他のモジュール)の処理手順の(機能的意味は変わらない)変更のせいでモジュール内部を変更する必要が生じることは無い、ということ、もう一つには、モジュールの内部的手順の(機能的意味は変わらない)変更のせいでモジュールの外(ないし他のモジュール)の変更が必要になることも無い、ということが、モジュラリティーが高いということだといえると思われる。裏からいえば、モジュールの機能的意味を変えずにその内部手順を変更できるという特性が、モジュラリティーであろう。また、こういった機能分割の前提として、モジュールの「機能的意味」の最小単位が十分に小さい、ということが必要である。あるモジュールは、更に小さいモジュールの集まりとして構成されていると考えられる。この階層を、モジュラリティーを崩さずに、非常に小さい機能単位まで降りていくことができる場合、システムとしてのモジュラリティーが高い、ということができる。この意味でみても、forthはシステムとしてのモジュラリティーが極めて高いのである。

「Forthでは、プログラマーがコンパイラーやインタープリターの動作まで変更できる」と言われることがある。
これがコンパイラーやインタープリターに当たるコード部分を直接書き換えるという意味であれば、それは極めて危険である。というのは、インタープリターは全てのコード、ワードを読み込む部分であり、その手順を書き換えれば、今まさに変更しようとしている部分とは関係のないワードの処理にも影響が出る可能性は大きいからである。そのようなことは、普通はしない。というより、できない環境の方が多いだろう。

ではどういうことか。
これは、むしろ、
プログラマーが普通に定義するワードで、ソースコード解析をして適当なコードをコンパイルしたり、その準備操作をしたりすることができる
ということなのである。いわば小コンパイラーワードが定義できるわけである。そういったワードによって、コンパイラー自体を書き換えることなく、マシンインストラクションを必要に応じて追加できる構文を創出することができるのである。これは、上で触れた :CALLBACK などのワードで実際に行っていることである。

そのような"コンパイラの動作の変更"なら、他のワードに影響が出る可能性など考える必要はもちろんない。当のワードに限っての動作だからである。不要ならばそのワードを使わなければよいだけである。他は平素のままである。だから、「コンパイラーの動作まで変更できるので互換性が損なわれる危険が高いだろう」という推論は、因果関係の認識としては誤りである。
互換性問題は、同じ動作に違う名前を付けたり、同じワードの動作が変更されたりすることが可能だから生ずるに過ぎない。
とはいえ、確かに、ifやループ構造のみならず、左括弧 ( やコンマの意味まで自分で定義して書き換えることができる言語はforth以外ほとんどないだろうが。

これに対して、例えば、ワード名やその周辺に何か符牒を付け、それらを標識としてインタープリターが構文解析するという形のグローバルな変更を導入してしまうと、他への影響を消すことはできない。このようなグローバルな変更を志向するのは、発想としてモジュラーと正反対であり、特に、forthの志向には反するもののように思われる。

しかし、「普通の」プログラミング言語のアプローチ方法は、コンパイラ方式であれ、インタープリタ方式であれ、ほとんどが、そのようなグローバルな発想一辺倒である。というのも、インタープリターないしコンパイラーが理解できる構文だけが意味があるのであり、すべてはそこで予め決まってしまっていなければならないからである。
個人的には、言語仕様の規格変更としてであっても、モジュラリティーに反する変更を要する規格には抵抗を覚える。
もっともMopsのメソッドセレクターはこの方法用いているが、iMopsでは他のワードへの影響を減らすため辞書探索順位を変更した。
標準forthでは、$や#を数値リテラルの先頭に(空白なく)付けることで進数指定できるようにしている。これはしかしワードにしてもできる
ことであるのに、わざわざインタープリターの内部機構を変えなければならない方法を採用したという点で、あまり好きではない。

ともあれ、モジュラー性が高いということは基本であるように思われる。柔軟性とか、動的変更とか、ダイナミックな運用(パラレルも含む)などが商業宣伝的に誇張されることが多いように思われるが、それは、そういう動作手順を考えればよいというだけ(簡単ではないが)であって、そのような発想の様々な手順による処理を困難なく導入できるためには、各部分の独立性が高いことは重要なのである。モジュラー性を高めるという志向でコード設計することが、結局、様々な「新しい」機能の共通の基盤となっているのではないかと思う。


タグ:

forth modularity
最終更新:2019年01月20日 08:39