snowlt23's website   About

Forthの覚醒 : road to jedi ;

思いつくままに書いていったら結構長くなってしまいました。Forthでよく言われる考え方について書いてみました。ちなみに考え方の部分は他とは区別するために意図的に「だである」調で書いてます。それではどうぞ。


プログラミング言語Forth

Forthというプログラミング言語がある。マイナー言語探訪を趣味にしていない限り、聞いたことがない人が大半だろう。
初めてForthの名を聞く人は最初にこう思うだろう。「どんな言語なの?」「どんな機能があるの?」と。
これに対する答えは非常に難しいが、あえて答えるなら私はこう答えるだろう。「Problem Oriented Programming Language(問題志向言語)」「あなたが望むであろう機能は一切無い」と。
ちょっとまってくれ、問題志向言語?どんなプログラミング言語も問題を解くためのものじゃないのか?それに欲しい機能が無いって、それは使う価値がある言語なのか?それはおもちゃ言語ではないのか?
おそらくこういう反応が帰ってくるであろう。その見方はもっともだ。どんな言語も問題を解くためのものだし、機能が無い言語は現実的な問題には使いづらいと感じるのも仕方ない。
しかし、それは既存のプログラミング言語によって形成された価値観のもとでの見方でしかない。ForthにはForthなりの考え方(Forth way)がある。
郷に入っては郷に従えということで、今回の記事ではそれについて語っていきたいと思う。

Forth入門

Forthがどんな見た目をしていて、どんな動作をするのかが他のプログラミング言語をやってきた人にはわかりやすいはずなので、まずはForthの動作を見ていこうと思う。

Forthで1 + 1をするプログラムは以下だ。

4 5 +

おそらく+を後ろに書くことに違和感を覚える人が殆どだろう。これは逆ポーランド記法と言われる書き方で、殆どの言語が演算子を中置にするのに対して、Forthでは後置にする。(反対にLispは前置) ただ記法中心に考えるのはここではあまり意味をなさない。それはForth wayではない。ここで重要なのは今書いたプログラムの動作だ。
ここで書いたプログラムは以下のような動作をする。

4をスタックに積む -> 5をスタックに積む -> スタックから2つの値を取り出し足し合わせる

スタックはプログラマならとても馴染みの深いデータ構造だ。スタックには積む(push)動作と取り出す(pop)動作の2つがある。この2つの動作を中心としてデータを扱う。
ここで重要なのは、Forthはスタックとともに処理が進行していく言語だということだ。上記のただ足し合わせるプログラムでもスタックを使って処理が進行する。多くのプログラミング言語では通常スタックを意識することは少ないだろう。(内部的には使われているのだが) 多くのプログラミング言語では足し合わせる処理は左の値と右の値を引数として+関数に渡すという処理の流れをとることが多い。
なんでこんな面倒なことをするんだろう?と思った方は多いのではないだろうか。このForthの動作は、プログラムを単純な関数の並びとして分割することを可能にする。先程の例に戻ろう。

4をスタックに積む -> 5をスタックに積む -> スタックから2つの値を取り出し足し合わせる

このプログラムの見方を変えるとこうなる。

4という関数を呼び出す -> 5という関数を呼び出す -> +という関数を呼び出す

そう、ただ関数を並べて順繰りに呼び出しているだけだ。そして関数がそれぞれスタックに値を積んだり取り出したりして処理が進行していく。そこには式や文といった概念は存在せず、ただスタック操作を行う関数呼び出しに完結するプログラムができる。(ちなみにForthでは関数とは呼んだりせずword(単語)と呼ぶことが殆どだ)(つまりForthのプログラムは単なる単語の羅列だ(dictionary(辞書)ともいう)

この動作はコンピュータの動作原理を反映したものである。プログラミング言語はコンピュータで動くものを作るためのものと教わることは多いかと思うが、実際には多くのプログラミング言語はコンピュータの動作原理をそのまま反映したものではない。多くのプログラミング言語は形式言語から出発しており、人が読みやすい(と思われている)自然言語へと近づけることに努力されてきたように思う。そしてそれをコンピュータで動く形式へと翻訳する処理系を通じて動くという仕組みだ。 しかし、Forthは自然言語へと近づけることを選択せず、あくまでコンピュータの動作原理に沿うことを選んだ。Forthはプログラミング言語ではなくリッチアセンブラだと言われることもあるが、これは大体正しいと思う。

しかしアセンブラでプログラミングしたいという人はどれだけいるだろうか?おそらく殆どいないんじゃないだろうかと思う。しかしアセンブラでプログラミングをしたいと思えないのは、それに抽象化能力が欠けているからだ。Forthはアセンブラのようなものと言われることがある。しかし、Forthにはどの言語にも負けない強力な抽象化能力がある。

: add5
  5 +
;

これはただ5を足すだけの関数(word)だ。:で関数定義の開始、;で関数定義の終了だ。しかし、Forthでの関数(word)には2つの種類がある。通常のwordと、もう一つがimmediate wordだ。
immediate wordにするには以下のようにする。

: add5
  5 +
; immediate

追加されたimmediateもwordだ。これは最も新しく定義されたwordのフラグを書き換える作用がある。(ちなみに:;もwordだ。関数定義のための特殊構文に見えるかもしれないが、順々に実行されているにすぎない)
immediate wordとは何なのか?それはコンパイル時に動作するwordだ。

: main
  4 add5
;

上記のmain wordでは、mainが実行されたときに4に5を足して9にするというのが期待される動作のはずだ。しかし、immediate wordにするとコンパイル時にwordが動作する。つまりmainというwordを定義している最中にadd5 wordが実行される。

こんなことができて何が嬉しいのか?察しのいい方ならもうすでにお気づきだと思うが、そう。メタプログラミングができるのである。
これはただ単に5を足すというなんの面白みも無いプログラムだが、コンパイル時にwordが動作するということは、コードを生成するwordを作って、それをimmediate wordにしてやれば、自在にプログラムを生成することができる。プログラムを生成することが可能になれば、自在にプログラムの抽象度をコントロールすることができる。

Forth哲学

ここで話題を変えて、大胆な主張をしてみたいと思う。(実際にはForth作者のチャールズ・ムーアの受け売りだが)

これを見て多くの人は「真剣に言ってるの?変数を使わないプログラムなんて書けないよ!それにコードは再利用するものでしょ?」と思うだろう。
ここにはちゃんとした考え方がある。ここで一番重視される考え方はシンプルさだ。 シンプルさとは一体何なんだろうか?まずは、簡単という意味ではない。

ここでの考え方は、

あたりだろうか。上記の3つの主張はこれらのためにある。

変数は多くのプログラミング言語の中心概念であるように思う。しかしForth作者のチャールズ・ムーアはこのような考え方を持っている。

変数を使わなければいけないコードを書いている時点で、関数に分割することを考慮するべきです。

変数を使わないという制約のもとでは、非常に短く、簡潔な関数定義へと繋がる。これは自身でForthを書いてみることで実感したが、変数を使わないプログラミングは、頭を使うが、それと同時に非常に綺麗で細かな関数定義へと分割される美しさを感じた。Forthでは多くのwordは1~3行で定義され、長くても6行程度となっている。そして変数というものが無くなって読みにくいかと思われたソースコードが、綺麗に分割され、非常に読みやすくなっていることに気づく。これは体験してみないとわかりにくいと思うので、気になった方はぜひForthを書いてみてほしい。

コード共有をするなとはどういうことだろうか。チャールズ・ムーアとしての考え方は、「汎用的に作られたプログラムは、膨大で理解しづらく、そしてあなた自身の問題に適合しない」とのことだ。
あなた自身が書いた自分の問題を解くためのプログラムは、高度に適合し、また、非常に小さく簡潔で、少ないコンピュータリソースで動くものが出来上がる。
ちなみにここで憶測をしないことも重要になってくる。あなたは未来を予知することはできない。だから、実現するかもわからない未来のためのコードを書くべきではない。ということだ。不必要なコードはプログラムを肥大化させ、また、負債化することが多い。

Forth Programming Language

Forthは機能が無い(正確に言うと最小限の)プログラミング言語である。しかし、強力な抽象化能力を備えている。 前述のForth哲学と照らし合わせると、Forthは「問題を解くための言語を定義するための言語」ということだ。つまり、「自分のことは自分でやれ」ということに尽きる。 Forthはプログラマに最大限の自由を与える代わりに、プログラマに全ての責任を委ねる。Forthには型が無い(動的型付けではなくて、型という概念そのものが無い)し、最近のGoやRustみたいな安全で気の利いた機能もない。

つまり、Forthではあなたが全てを決めるのだ。そこには言語の制約や機能が邪魔をすることは一切無い。これを自由と見るか危険と見るか不便と見るかはプログラマ次第かも知れないが、他のプログラミング言語と違って非常にユニークだということは多くの人に同意してもらえると思う。

Forthは、問題に適合することを選んだ言語なのだ。


実用的な話

まぁなんか色々書きましたが、つまり言いたいのはForthは問題に適合するための言語を作る言語という側面が強いということです。
Forthの言語的な特徴としては、

あたりでしょうか。Interactiveとかは結構重要で、Forthコードを書いてすぐ試したり、プログラムが動いている最中に動的にプログラムを生成したりとか色々自由なことができます。

また、Smallというのも現代の言語の多くで失われた特徴で、Forthプログラムは非常に小さくなることが多く、また、言語が単純なので組み込み環境の少ないメモリでもコンパイラ全てが収まる容量にすることが可能です。なのでForthを組み込み環境で動かしてる人はそこそこ見かけます。他のインタラクティブな言語の多く(SmalltalkやCommon Lispなど)は全てを含めたイメージにすると結構巨大になってしまうのですが、Forthは現代でもSmallさを保っています。 あとプログラムが小さいということは人にとってもメリットがあって、単純に1人の脳に全てがおさまるプログラムになるというメリットもあります。人間にもコンピュータみたいなメモリがあると思っていて、あまりにも複雑なプログラムだとプログラム全体について考えることを阻害するように思います。
特に自分は一般的なメジャー言語の多くでプログラムする際は結構ストレスがあって、単純に言語自体が巨大な言語が多いので、パーサですら脳内に入りきらない感覚をうけます。それに対してForthは言語全体が人間のL1キャッシュに載りきるサイズのような感覚です。
これはいわゆる標準ライブラリをあるていど暗記するようなもので、必ずしもやる必要はないが、やるのとやらないのではコーディング速度に影響が出て、最終的な成果物にも影響が出る感覚です。

Forth哲学はRui Ueyamaさんがブログに書かれている考え方にも近いように思います。こういった考え方はForthだけでなくソフトウェア開発に普遍的に見られるものとも言えるかもしれません。
Link ->

処理系

Forth処理系は数あるプログラミング言語の中でもトップクラスに処理系が作りやすいプログラミング言語なので、色々処理系があったりします。(まったく名前を聞いたことのないチップで動く処理系とかも見かけます) ですが、とりあえずこれを使っておけばOK的な処理系は無いなぁという印象を受けました。
自分が試した中で、WindowsやLinuxといった標準OSで動かす場合の処理系は以下がおすすめです。

ちなみに今自分自身でForth処理系を作ってみてます。現状でも標準ライブラリはある程度動くところまではいったので折を見て公開する予定です。(多分単純な最適化実装できたあたりかな)

文献

日本語の情報が本当に少ないので英語メインにはなりますが、色々見た感じ以下がおすすめです。(というか英語でも本当に情報が少ない・・・)

方言

ちなみに言語自体がシンプルで作りやすいからか方言もいろいろ生まれてます。
方言で有名なのは動的型付けのFactorと静的型付けのKittenでしょうか。これらはForthの影響を受けてますが全く違う言語なのでこちらもやってみると面白いかもしれません。


Forthすごい良い言語だなぁと思ったんですが、あまりにも情報が少なすぎたので色々書いてみました。 Forthは面白い言語なので言語自体のHackな記事とかも書いてみたいですね。ForthでWebAssemblyいじりとかやったりしてるので、それもできれば記事にしたいです。
明日から仕事で使える知識/技術/言語というわけでは絶対に無いですが、まぁ楽しみのためだとか、はたまたガチで使ってみるのもいいんじゃないかなぁとか思います。 Good Luck Have Hack :)

追記

知り合いからForthのimmediate wordの動作が気になると指摘されたので、Forth処理系がどんなプロセスで処理を進行させるか書こうと思います。

Forthでは:wordでcompilation stateというものに入ります。コンパイル状態ということですね。反対に;でcompilation stateから抜けます。
このcompilation stateでのword呼び出しは、そのwordを呼び出す機械語コードやVMコードに変換されます。しかし、このwordがimmediate wordであった場合、呼び出しコードに変換する代わりにその場でwordを実行します。こうすることでword定義の中でwordの実行が可能になります。
C言語など多くの言語では、ソースコードをコンパイル->リンク->実行ファイル->実行という流れをとることが多いですが、Forthではword1つ1つを呼び出すたびにコードの生成が行われ、実行が可能になります。ある意味ではJITコンパイラとも呼ぶことができますね。

Written January 29, 2018. Send feedback to @snowlt23.

← Supercon2017参加記