読者です 読者をやめる 読者になる 読者になる

レガシーコード生産ガイド

私に教えられることなら

Forth + プロトタイプベースなFactor風言語を作りました

JavaScriptで組みました。動作ページ

前書き

  • エラー処理はまだないので、コンソールを開いて確認してください。
  • 名前は未定です。(とりあえずPrototype + ForthでProtoForth)
  • とりあえずの実験なので、何度か全体を書き直すと思います。
  • Factorを始めとした、プログラミング言語自体をある程度知ってる人向けの説明です。
  • まったくの素人の独学なので、以下での用語や理論の使い方は間違ってる可能性があります。指摘してもらえると嬉しいです。
  • もうForthとプロトタイプベースオブジェクト指向を混ぜたものがある場合、それを教えてもらえるとありがたいです。

以下のコードをコピペすると、使用例のようにワードの定義などが楽にできます。以下、全てのコード例は以下のコードを前提とします。

[ ->word parse-word: read def! ] ->word parse-word: 
def> read def! [ parse-word: read ] def> word: 
[ word: set! ] def> set> [ word: quotation @curry
@call word: swap @set! ] def> set-in> [ rot rot ] def> 
-rot [ -rot @call ] def> if [ quotation if ] def> when
 [ quotation swap if ] def> unless

動かし方

単純にスタックベースのforthとして使えます。

(数字と文字列を認識しますが、文字列は半角スペースを含めません。また、最後のメッセージ以降に半角スペースなどがあっても例外になります。単なる手抜きなので多分今後修正します。)

> 1 2 +
3

使用例(動作ページでは上に追加されていきます)

スロット書き換え

> 4 set> hoge

> hoge
4

条件分岐、ワード定義

> 5 4 > [ "yes" ] [ "no" ] if
"yes"

> drop

> [ 1 > [ "そのようだ。" ] [ "そうでもない。" ] if ] def> 1より大きい?

> 4 1より大きい?
"そのようだ。"

再帰

> [ dup 0 <= [ drop ] [ 1 - "hoge" swap n-hoge ] if ] def> n-hoge

> 5 n-hoge
"hoge""hoge""hoge""hoge""hoge"

基本設計

プロトタイプベースのオブジェクト指向と、Factorの様なConcatenative/Quotationをベースにしました。

オブジェクトにメッセージを送ることでプログラムを作っていきますが、コードを評価してコンピュータ内のオブジェクトを外から操作するのではなく、オブジェクトの内部に入ってそこを書き換えていくイメージです。

メッセージ送信

スペースで区切られた単語は全てメッセージです。そのまま書くと、プログラマが居るオブジェクト(self)へのメッセージになります。

hello

JavaScriptコンソールへhello world!を出力するメッセージです。

self

プログラマが居るオブジェクト(self)をスタックトップに置きます。

メッセージの前に@をつけると、スタックトップに置かれているオブジェクト(レシーバ)にメッセージを送信します。

self @hello

この場合はスタックに自分自身を置き、それに対してhelloメッセージを送信します。単にhelloと書くのと同じ意味になります。

スロット

それぞれのオブジェクトは、メッセージ名に対応したスロットを持ちます。例えば動作ページで最初に入っているオブジェクト(self)には、helloスロットがあります。

self @helloによって、まずselfからhelloスロットを探し、発見したらhelloスロットに入ってるもの(word)を実行します。関数のようなものです。

また、以下のように変数を定義・使用できますが

> 3 set> foo

> foo
3

これは、「3を返すワード」を自身のfooというスロットに入れて、使っています。

プロトタイプ

以下のメッセージを送ると、送られたオブジェクト(レシーバ)のプロトタイプをスタックに置きます。

prototype

プロトタイプベースオブジェクト指向の詳しい説明は他に譲ります。全てのメッセージは、送られたオブジェクト(レシーバ)が処理できない場合は、そのプロトタイプに送られます。そのプロトタイプが……と伝播していき、最終的に全ての大元であるObjectオブジェクトが処理できなかった場合、エラーとなります。

その様子は次で見ていきます。

クローンと移動

cloneメッセージはスタックトップにあるオブジェクトのクローンを作ります。

> self clone 

これでスタックトップにselfのクローンが置かれた状態で、<ENTER>メッセージを自身に送ると、スタックトップに置かれているオブジェクトに入れます。(どのオブジェクトに送られても、プログラマが居るオブジェクト(self)をスタックトップのオブジェクトに変えます)

> <ENTER>

オブジェクトはそれぞれのスタックを持ちます。<ENTER>する前に何か積んでいたとすると、それは消えたように見えるでしょう。<LEAVE>で脱出し、前に居たオブジェクトに戻ると、スタックの内容は復活します。以下のようになります。

> 5 5 5
5 5 5

> self clone <ENTER>

> 4 4 4
4 4 4

> <LEAVE>
5 5 5

クローンの中でスロットを定義しても、元のオブジェクトには影響がありません。

まず後で呼び出せる様にクローンを自身のスロットに入れておきます。

> self clone set> c1

クローンの中でスロットを定義し、中と外でそれぞれクローンにメッセージを送れる事を確認します。

> c1 <ENTER>

> 9 set> yeah

> yeah
9

> <LEAVE>

> c1 @yeah
9

一方、戻ってきたオブジェクトにメッセージを送ると、自分のプロトタイプより前からしかメッセージを探せないのでエラーにります。(現在はJavaScript例外が投げられます)

> yeah
(エラー)

クォーテーション

クォーテーションオブジェクトは、Factorのクォーテーションや関数、関数の合成みたいなもので、[]に挟まれたメッセージ列を、実行せずに保存することができます。[ ]はどちらもメッセージなので、半角スペースを入れる必要があります。

> [ 1 2 + ]
[object id:113] (idはその時で変わります)

クォーテーションオブジェクトに@callメッセージを送ると、実行することができます。

> [ 1 2 + ]
[object id:113]

> @call
3

> [ 1 + ] @call
4

quotationメッセージで新たな空のクォーテーションオブジェクトを作れます。それにcurryメッセージを送ると、スタックの次にあるものを合成することができます。

> 1 quotation @curry
[object id:131]

> @call
1

この場合、[ 1 ]というクォーテーションオブジェクトが作成され、それに対して@callメッセージが送られたので、その中身の1というメッセージがselfに送られ、スタックに1が積まれます。

> 3 1 [ + ] @curry @call
4

この場合は、[ 1 + ] というクォーテーションオブジェクトが合成され、@callによって作動し、3 1 +で4、という流れになります。

(現在、ネストしたクォーテーションオブジェクトで@curryなどを使うと変更が残ってしまいます。多分今後修正しますが、quotationメッセージで新たなクォーテーションを作って使うようにすると防げます。)

メッセージ定義

def>によって、クォーテーションオブジェクトを自身のスロットに入れて、メッセージを処理させる事ができるようになります。

クォーテーション def> メッセージ名(スロット名)という順番です。

> [ 1 + ] def> inc

> 5 inc
6

(前に定義したselfのクローンc1が残ってる場合、c1も新たなメッセージを(selfによって)処理できるようになっている……はずですが、で中に入らないと処理できません。多分今後修正します。)

現在の構成、メッセージ

Object - Quotation - Boolean - Math - Loungeという継承構成です。プログラマはLoungeにした状態から始まります。

また、Wordオブジェクトが別に存在し、メッセージ(を処理するワードの)定義に使われています。

Objectオブジェクト

  • clone スタックトップにあるオブジェクトをクローンしたオブジェクトで置き換えます。
  • dup スタックトップにあるデータを複製します。単純に参照のコピーです。
  • swap スタックの1番目と2番目を入れ替えます。
  • rot 上からa b cと積んであるスタックをb c aと入れ替えます。
  • drop スタックトップのデータを捨てます。
  • parse-word: 次の1トークン(半角スペースで区切られた文字の列)を読み込み、文字列としてスタックトップに置きます。
  • read スタックトップの文字列を読み込み、内部データとして置きます。スロット操作用のシンボルの作成に使っています。
  • set! selfのスロットに、データを返すワードを入れます。value symbol set!として使います。
  • def! selfのスロットに、ワードとみなして直接入れます。word symbol def!として使います。
  • self プログラマが現在居るオブジェクト(self)をスタックトップに置きます。
  • <ENTER> スタックトップのオブジェクトに入ります。
  • <LEAVE> オブジェクトから抜け、<ENTER>する前のオブジェクトに戻ります。
  • Object Objectオブジェクトをスタックトップに置きます。
  • prototype レシーバのプロトタイプをスタックトップに置きます。prototypeとだけ送信した場合はselfのプロトタイプです。prototype @prototypeならselfのプロトタイプのプロトタイプが置かれます。
  • take-in 対象オブジェクト自身が持つスロットの中身を、レシーバにコピーします。target symbol take-inでselfにコピーします。シンボルの作成はparse-word: hoge readで行えます。
  • take-in-all 対象オブジェクト自身が持つ全てのスロットの中身をレシーバにコピーします。target take-in-allでselfにコピーします。

Quotationオブジェクト

  • curry スタックトップのデータを、自身の先頭に取り込みます。1 [ + ] @curryなら、[ 1 + ]と同じ状態になります。
  • call 自身を実行します。5 [ 1 + ] @callなら、スタックトップに6が置かれます。
  • [ 半角スペースで挟まれた]が来るまでトークンを読み込んで、それらで構成された新しいクォーテーションオブジェクトを作ります。(ネストも可能ですが、現在動作は怪しいです。)
  • ->word スタックトップのクォーテーションオブジェクトを、スロットに入れてメッセージ処理に使えるワードに変換します。
  • quotation 新しいクォーテーションオブジェクトを作ります。curryメッセージをクォーテーションオブジェクトの中で使う場合は、こちらの使用が安全かもしれません。

Booleanオブジェクト

  • eq? スタックの1番目と2番目を取り、JavaScriptの===で等価なら下のtrueが作るクォーテーションオブジェクトを、そうでないならfalseが作るクォーテーションオブジェクトを置きます。
  • true スタックの1番目と2番目のクォーテーションオブジェクトを取り、1番目を捨てて2番目を@callするクォーテーションオブジェクトをスタックトップに置きます。
  • false 上の逆で、1番目を@callするクォーテーションオブジェクトをスタックに置きます。

要するに、λxy.xλxy.yです。[ 1 ] [ 2 ] 10 10 eq? @callで1がスタックトップに積まれます。ifはクォーテーションの順番を入れ替えて@callしているだけです。

= > < >= <= はそれぞれ上のtrue/falseクォーテーションオブジェクトを置きます。3 4 <ならばtrueです。

Mathオブジェクト

+ - * /二項演算子と、abs sqrt random floor ceilの単項演算子が定義してあります。randomはスタックトップの数字とJavaScriptのMath.random()の積を置きます。

Loungeオブジェクト

テスト用のhelloが定義してあります。JavaScriptコンソールにhello world!します。

ユーティリティの解説

コメントシンタックスがないのでそのまま書きます。

[ ->word parse-word: read def! ] ->word
parse-word: def> read
def!

メッセージを定義できるワード「def>」です。
まずこの時点では、word symbol def! による定義しかできません。
wordはクォーテーションを->wordで変換して作り、
symbolは parse-word: で1トークン読み込み、readで変換します。
それが1行目のクォーテーションの中身と、全体の構造になります。
同じ構造で包んでいる、ということです。


[ parse-word: read ] def> word:

1トークン読んでシンボルに変換するワードです


[ word: set! ] def> set>

値を返すメッセージ、を定義することで値そのものがスロットに入ってるように見せかけます。
これも value symbol set! で定義する方法を短くわかりやすく変えています。


[ word: quotation @curry @call word: swap @set! ] def> set-in>

自身のスロットに入ってるオブジェクトにset>します。
word:でスロット名を読み込み、quotation @curry @callで 
クォーテーションオブジェクトにして実行します。
例えば clone-1 にクローンが入ってるなら、これでclone-1のスロットの内容が
スタックトップに置かれます。
どちらかというとその動作のテスト目的です。


[ rot rot ] def> -rot

forthのワードです。


[ -rot @call ] def> if

true/falseオブジェクトへの@callだと、[ true時 ] [ false時 ] 条件 @call
という順番になるので、スタックトップの条件が使いづらくなります。
これで 条件(true/false) [ true時 ] [ false時 ] ifと書けるようになります。

[ quotation if ] def> when
[ quotation swap if ] def> unless

lisp由来です。不要な[ true時 ]や[ false時 ]を排してコードを読みやすくできます。

思想、経緯など

ここからは全て「私自身は」という話になります。

以下の性質が好きなので、それらを持つ言語を触ったり調べたりしています。

  • ミニマルなシンタックス
  • シンプルな言語仕様
  • 言語自体を拡張可能

私はとにかく複雑なものが苦手で、身の回り含めてシンプルに保ちたいのですが、やはりプログラムが大きくなるとその複雑さが「嫌に」なります。

複雑さへの対処として上の要素を追い求めて居ましたが、ふとソースコードとプログラム間だけでなく、その複雑さを認識する人間(私)に注目したらどうかな、と思いました。

住んでいる町の入り組んだ道や多くの人との関係などは、混乱することもありますが、複雑さの割には、絡まったケーブルや複雑なプログラムから感じるタイプの嫌な感覚はしません。

複雑な動作(関数)やオブジェクトの関係でプログラムを組むとき、その中に入る感覚を掴むことができれば、場所の感覚や社会性など人間自身が持つ能力を発揮できるかもしれないと考えました。

そのためにプログラムを書く視点を逆転させ、で今居る場所をメッセージ送信によって書き換えていくスタイルにしました。

また、最初に整理された構造を考えるのが(訓練次第かもしれませんが)まだ苦手なので、「まずオブジェクトがあって、それを整理できる」ように、プロトタイプベースを選択しました。整理する仕組みも現在考えています。

プログラマ→コード→(処理)→プログラムの動作 というフローを意識すると、どうしても私は俯瞰的な見方のみになってしまうので、主観的な視点をベースとして、地図の様な俯瞰も提供できるプログラミング環境全体の構築を目標にしています。

なので、言語仕様やシンタックスの開発だけでなく、よりオブジェクトの中に居ることを意識できる環境の開発も進めていきたいです。

Factor(Forth)の「全てのワードは逐次実行される」という仕組みを一番のベースにしたのは、全てをメッセージ送信にする、という理想を実現できるのではないか、と考えたからです。また、上に挙げた3つの性質満たせるからでもあります。

他にもCommon LispClojureSmalltalkの影響を受けていて、これからもその影響は強まると思います。

感想、課題

  • ある程度安心して使えるところまでは作って、それからこのやり方を続けるか判断します。
  • オブジェクトの階層構造はもっとシンプルにできそうなので、全体を作りなおすと思います。
  • オブジェクトによるリーダーマクロや、クラスブラウザ的なもの、Ideal Hash Triesを使ったクローンなど、アイディアがいくつかあるので継続していきたいところです。
  • メッセージをスタックトップのオブジェクト自身に送るか、スタックトップを処理するかの部分で整合性が取れないのが残念なところです。実用性とのトレードオフになるか、解決策が思いつくか、まだ考えていきたいと思います。
  • クォーテーションだけでかなりの表現ができる予感がするので、ベースはオブジェクトではなくクォーテーションになりそうな気もします。
  • 最初に思いついたときは途方も無い夢物語に思えたのですが、ダメ元で取り組んでみたら構想の一部を実現できたので感動しました。楽しい。
広告を非表示にする