F/L != Forth
F/L も Forth を祖先に持つ言語ですが、思うところあって色々変更していったら、最早 Forth の方言という範囲には収まらなくなってきたように感じます。そこでこの文章では、主に Forth と異なる部分を中心に F/L でのプログラミングについて説明していきます。
注:Forth のモダンな拡張としては Joy や Factor などがあるかと思います。私もこれらの言語について調べているところですが、なかなか良い教科書が見つからず、学習が進んでおりません。識者の皆様におかれましては、これは Joy のサブセットですね、とか Factor のサブセットだよね、などという感想もあろうかと思います。このようなご意見は大歓迎しますので、コメントなどいただければ幸いです。
なお、F/L 言語の実装の元ネタとなっているのは、先に作成した「35 行の Lua プログラムで Forth インタプリタを作る(内部インタプリタ 6 行、外部インタプリタ 29 行)」です。
型による実行ワードの決定
Forth では、ワード名により起動されるワードは決まっていました。しかし、F/L ではスタックに様々な値を積めるため、ワードによっては、積まれている値に対応できない場合があります。典型的な例としては . (ドット)があります。
数値や文字列を表示するワードと、リストを表示するワードは動作が異なります。そのため、F/L ではデータスタックの TOS の値がどのような型であるかにより、起動するワードが決定されます。例えば、TOS がリストであれば、list$. が起動されます。F/L ではドル着号 $ は辞書とワードを区切るセパレータの役割をします。この例では、list 辞書にあるワード . (ドット)を意味します。
TOS の型に応じたワードが見つからない場合は、object 辞書を検索します。object 辞書でも該当するワードが見つからない場合は、該当ワード無し、ということでエラーとなります。
ユーザー定義のワードは、ワード名中に辞書が指定されない限りは object 辞書に登録されることとなります。また、即時実行特性(immediate 特性)を持つワードも object 辞書に登録されています。これらのワードはコンパイル中でも実行されるため、その時の TOS の型に無関係に実行されないと、プログラミングが非常に複雑になるためです。
コメント用ワード
Forth では ( と ) に囲まれた部分はコメントとして認識されました。F/L では、( と ) はリストの表現に使用します。そのため、コメントは C から記法を拝借し、/* と */ で囲まれた部分をコメントとして解釈するようにしました。
データ型
Forth には整数値とワードしか存在しませんが、F/L ではワードの他に、実数、リスト、無名ワードブロックもスタックに積むことができます。これら様々な値を取り扱うことが可能となったため、Forth とは異なる言語に変化していっている状況です。
F/L> "hello" .
hello ok.
F/L> ( 1 2 3 ) .
( 1 2 3 ) ok.
F/L> [ 2 3 * . ] execute
6 ok.
注:実行例における F/L> は F/L インタプリタのプロンプトを表しています。
実数
lua の tonumber() 関数で数値に変換される文字列は数値として取り扱われます。Forth と異なり、浮動小数点値もスタックにプッシュできます。
F/L> 5
ok.
F/L> .
5 ok.
F/L> -1.23e4
ok.
F/L> .
-12300 ok.
文字列
ダブルクォーテーションで囲まれた文字列は、F/L でも文字列として認識されます。文字列も値として取り扱われますので、スタックにプッシュできます。
F/L> "this is a string"
ok.
F/L> .
this is a string ok.
文字列はワード名を示す用途などにも使用されます。次の例は、データスタックにプッシュされた 2 という値を表示するために、ワード .(ドット)を "." という文字列から起動する例です。@word はデータスタックの TOS にある文字列をワードとして解釈し、そのワードそのものをデータスタックにプッシュするワードです。execute はデータスタックの TOS にあるワードを実行するワードです。"." @word execute で、結果としてワード . (ドット)が実行されます。
F/L> 2
ok.
F/L> "." @word execute
2 ok.
リスト
大カッコでくくられた文字列はリストとなります。リストデータは外部インタプリタで処理されているわけではなく、ワード ( と ) によりリストが構成されるようになっています。数値、文字列、リストをリストの要素として使用できます。
F/L> ( 1 2 3 )
ok.
F/L> .
( 1 2 3 ) ok.
F/L> ( "this" "is" "a" "pen." )
ok.
F/L> .
( this is a pen. ) ok.
F/L> ( 1 ( 2 3 ) 4 5 )
ok.
F/L> .
( 1 ( 2 3 ) 4 5 ) ok.
ダブルクォーテーションで囲まれない文字列はワードとして配置されるため、ワード . (ドット)で表示すると正しく表示されません。
F/L> ( this is a pen. )
ok.
F/L> .
( ( ) ( ) ( ) ( ) ) ok.
ワードのリストは、ワード >noname を用いて、後述する無名ワードブロックに変換できます(つまり、実行できます)。
F/L> ( 2 3 * . ) >noname execute
6 ok.
(値としての)無名ワードブロック
無名ワードブロックの実態はこのようにリストそのものですが、無名ワードブロックを作る時に毎回 >noname を呼び出すのも煩わしいです。そのため、F/L では、[ と ] を用いて、無名ワードブロックを作成できるようにしています。
F/L> [ 2 3 * . ] execute
6 ok.
(対話環境のための)無名ワードブロック
中括弧で囲まれた文字列も無名ワードブロックとして扱われます。F/L では Forth 同様、if やループに関するワードはコンパイルモードでしか使用することはできません。しかし、ちょっとした処理や単にアルゴリズムを試したいときなど、わざわざ新しくワードを定義するまでもない、という場合があります。そのような時、無名ワードブロックを活用できますが、[ 〜 ] の後に、execute ワードを記述するのも煩わしいです。そのため、対話環境用の無名ワードブロックとして、中括弧を用いる記法を用意しています。使用方法は以下の通りで、制御構造を含むプログラムを { 〜 } で囲み、実行します。
F/L> 1 false { if . then }
ok.
F/L> 1 true { if . then }
1 ok.
F/L> false 1 2 3 { while . loop }
3 2 1 ok.
F/L> { 1 10 from-to foreach . next }
1 2 3 4 5 6 7 8 9 10 ok.
ワード { や } は即時実行特性を持つため、無名ワードブロックをスタックに積むことができません。値として無名ワードブロックを使用する場合は、先に述べた[ と ] を用いて、スタックに無名ワードブロックをプッシュします。
スタックに積まれた無名ワードブロックに名前を与えるワードとして assign があります。ワード assign は、データスタックの 2 番目にある無名ワードブロックを TOS にある文字列のワードに割り当てます。
F/L> [ dup * ] "square" assign
ok.
F/L> 5 square .
25 ok.
assign を使うと、ユーザー定義の新たなワードを作り出すことができます。しかし、assign を使うと、ワード本体の実装が先に記述され、その処理の名前(=ワード名)が次に現れることとなります。これは、一般的な言語とは順序が逆になりますので、ワード定義には、次に述べる := というワードの使用を推奨します。
ワードの定義
Forth では、「コロン記号(スペース)ワード名」という順序でワードの定義を開始します。F/L では文字列を扱うことが可能ですので、コロンの直後の文字列を特別扱いする必要はなく、スタック上の文字列をこれから定義するワードの名前と解釈することが可能になりました。
F/L では「これから定義するワード名(スペース):=」という順序でワードの定義を開始します。:= は即時実行特性を持つワードで、以後、セミコロンまでの間を、データスタックの TOS (Top Of Stack) に積まれている文字列をワード名としてもつワードを定義します。
F/L> "sq" := dup * ; /* TOS を 2 乗するワード */
ok.
F/L> 5 sq .
25 ok.
ワード := を用いるワード定義は、本質的に Forth におけるコロン定義と違いはありません。そのため、ワード := を用いて定義されるワードを、(F/L における)コロン定義されたワードなどと表現することとします。
#変数および定数についてと、ワードの上書可能属性について
Forth においては厳密には変数は存在せず、その変数によって格納されるデータの保存領域のアドレスを返すプログラム=ワードが存在するのみです。変数名(のワード)を実行すると、アドレスがスタックに積まれるため、そのアドレスから読み出す=フェッチ(ワード名 @)したり、そのアドレスに値を格納=ストア(ワード名 !)できたりします。一方、定数においては、格納されている値をスタックに積むワードとして定義されます。
F/L での変数のしくみはひとつではありません。Forth 由来の従来型のものと、後述する環境スタックを用いて実現されているローカル変数があります。ここでは、従来型の変数について説明を行います。
従来型の変数および定数は、F/L でもグローバルな変数および定数として実装されています。厳密に言えば、F/L には Forth 型の変数は存在しません。F/L における変数は、Forth でいうところの定数です。しかし、これらはリプログラム可能であるため、変数として利用できます。
しくみについては少しややこしいですが、使い方は至って簡単で、変数も定数も、単に値を返すワードとして定義すれば十分です。
"PI" := 3.14 ;
コロン定義の場合、デフォルトでは上書可能特性が与えられません。そのため、このままでは、書き換え不可能なワードとして定義される=定数となります。もし、この "PI" を上書き可能な値を返すワード(つまり変数)としたければ、writable もしくは >writable を用いて上書可能属性を付けます。
上書可能属性を持つワードについては、ワード set! を用いてスタックに積む値を変更することができます。set! は (n s -- ) なるワードであり、2 番目に積まれている値をプッシュするように、TOS で示されるワードをリプログラムするワードです。以下、実際に writable, >writable, set! を用いたサンプルプログラムを示します。
writable は直前に定義されたワードに上書き可能特性を与えます。
F/L> "PI" := 3 ; writable
ok.
F/L> PI .
3 ok.
F/L> 3.14 "PI" set!
ok.
F/L> PI .
3.14 ok.
一方、>write はデータスタックの TOS にある文字列と同名のワードに対して上書き可能特性を与えるワードです。
F/L> "PI" := 3 ;
ok.
F/L> 3.14 "PI" set!
F/L ERROR:can not overwrite. the word 'PI' is protected.
F/L> "PI" >writable
ok.
F/L> 3.14 "PI" set!
ok.
F/L> PI .
3.14 ok.
set! の内部実装について少し説明をすると、ワードの再定義に関してはリストと >noname および assign というワードが本質的な役割を演じています。set! は以下のように定義されています。
"set!" := swap () insert >noname swap assign ;
余談ですが set! というワードは scheme から拝借してきました(ビックリマーク(エクスクラメーションマーク)はバンと読む。set! と書いてセットバンと読みます、と scheme の授業で習ったのが懐かしいです)。
変数再び - ローカル変数編
F/L にはローカル変数があります。ワード do でローカル変数用の環境を環境スタック上に確保し、ワード end で関連する環境を破棄します。ローカル変数への値の格納は !var で行い、読出し(ローカル変数に格納されている値をスタックに積む)は @var で行います。
F/L> "mySwap" := do "tos" !var "second" !var
F/L>> "second" @var "tos" @var end ;
ok.
F/L> 1 2 mySwap . .
2 1 ok.
F/L> "tos" @var
F/L ERROR: can not found the environment (list) for 'tos'.
※ "F/L>>" というプロンプトはコンパイル中であることを示すプロンプトです。
上のサンプルプログラムではデータスタックの TOS と 2 番目を入れ替える mySwap を定義しています。tos というローカル変数に一番上の値を、second というローカル変数に 2 番目の値を格納しています。それらを @var で参照し、順序を入れ替えてプッシュしています。
mySwap が終了するときには、end ワードにて、ローカル変数 tos と second を保持していた環境は破棄されますので、tos というローカル変数を参照しようとするとエラーとなります。
環境スタックのもうひとつの使い方 - リターンスタックではなく環境スタックを使おう
ローカル変数を格納するのは環境スタックであることは上で説明しました。環境スタックはローカル変数を格納するだけでなく、一時的な値の退避場所としても使用できます。Forth の場合、このような役目をリターンスタックが担っていました。F/L でもデータスタックの TOS をリターンスタックへプッシュする >R や、その逆の R> も実装されていますが、情報の一時退避場所としては環境スタックを使うことをおすすめします。その理由ですが、リターンスタックは、コロン定義から復帰するために使用されるため、セミコロン(semis)実行直前には、ワード実行当初の状況に復元されている必要があります。
情報を一時的に、データスタックとは別の場所に保管したい場合は、ワード do を呼び出し、環境を作成して下さい。環境スタックへデータスタックの TOS をプッシュする >E や、その逆の E> を用いる場合、ワード end を呼び出す時、do 呼び出し直後の状態に復元する必要はありません。ワード end は最も上にある環境までスタックを縮めるため、プログラマは環境スタックの状態を意識する必要はありません。
制御構造
F/L の制御構造には、大きく分けると条件分岐とループが存在します。どちらもコンパイルモードでしか使用することができません。対話的に使用する場合は、対話環境用無名ブロックを構成する { 〜 } で囲まれた部分で使用して下さい。
条件分岐
条件分岐は Forth と同じです。使い方は、
if 真の時に実行されるワード列 then
もしくは
if 真の時に実行されるワード列 else 偽の時に実行されるワード列 then
という形で使用します。
F/L> "even?" := 2 % 0 = ; /* まず even? ワードを定義する */
ok.
F/L> 10 even? .
true ok.
F/L> 13 even? .
false ok.
F/L> 5 even? { if "EVEN" . else "ODD" . then }
ODD ok.
F/L> 12 even? { if "EVEN" . else "ODD" . then }
EVEN ok.
無名ワードブロックで囲まないとエラーになります。
F/L> 5 even? if "EVEN" . else "ODD" . then
F/L ERROR:'if' should be use on the compile mode.
余談ですが、無名ワードブロックを使用すると、factor のようにも書くことができます。
F/L> "factorIF" := rot if drop else swap drop then execute ;
ok.
F/L> true [ "true-part" . ] [ "false-part" . ] factorIF
true-part ok.
F/L> false [ "true-part" . ] [ "false-part" . ] factorIF
false-part ok.
ループ
while
while 〜 loop という形で使用します。データスタックの TOS が false になるまでループします。
F/L> false 1 2 3 { while . loop }
3 2 1 ok.
while も無名ワードブロックで囲まないとエラーとなります。
F/L> false 1 2 3 while . loop
F/L ERROR:'while' should be use on the compile mode.
foreach
foreach はデータスタックの TOS に存在するイテレータを取り込み、そのイテレータが false を返すまで、foreach 〜 next で囲まれた部分を実行するワードです。実際には以下に示す from-to などのイテレータ生成ワードと共に使用されます。また、Forth とは異なり、多重ループも特に意識することなく普通に使用できます。
foreach もフロー制御用のワードですので、インタプリタモードで使用するためには、無名ワードブロックで囲って下さい(コンパイルモードでは何も意識することなく使用できます)。
break
foreach 〜 next のブロック中で使用します。このワードを実行するとループから抜けます。
F/L> 1 10 from-to { foreach dup 5 > if break else . then next }
1 2 3 4 5 ok.
from-to
データスタックの 2 番目の値から、TOS の値まで、ひとつづつ増加するイテレータを作成するワードです。こちらもサンプルを見たほうが早いかと思います。
F/L> 1 10 from-to { foreach . next }
1 2 3 4 5 6 7 8 9 10 ok.
二重ループについても、ごく普通に使用できます。次の例は九九の表を作成する F/L プログラムです。
F/L> 1 9 from-to { foreach "i" !var
F/L>> 1 9 from-to foreach
F/L>> "i" @var * .
F/L>> next
F/L>> cr next }
1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54
7 14 21 28 35 42 49 56 63
8 16 24 32 40 48 56 64 72
9 18 27 36 45 54 63 72 81
ok.
lines
データスタックの TOS にあるファイルハンドルから一行づつ読み込むイテレータを作成するワードです。以下のサンプルでは、sample.fls の内容を画面に表示します。
F/L> "sample.fls" { fopen dup lines foreach . cr next close }
"test" := "hello, world." . ;
test
ok.
F/L>
おわり
以上で説明は終わりです。定義済みのワードに関しては、別記事をご覧下さい。