CREATE - DOES>
CREATEとDOES>を組み合わせる語法は、forth言語で出会う最初の驚きである。
CREATE - DOES>の対は、何かのワード定義の中で用いる。そのようにして定義されたワードは特別なワードとなり、CREATE-DOES>ワードなどとも呼ばれる。そのようなワードのどこが特別かというと、あるパターンのワードを定義(というより生産)できるワードになるのである。しかも、そこで生産されるワードは、実行コードの他に当該ワードに固有のデータ領域を持つのである。
CREATEは、変数の説明でも触れた、アレイを作るワードと同じワードである。このワードは宣言的に用いる必要があるわけではなく、通常のワード定義の中から呼び出すこともできる。そうすれば、その定義されたワードが実行されるときにCREATEも実行されるのである。ここで説明するCREATE DOES>構文においても、CREATEはデータ領域と結びついたワードを形成するのに用いられることになる。
他方、DOES>は、doの三人称単数型活用doesに「より大きい」記号がついたものである。これは、「すること」、つまり動作を定義することを示唆している。DOES>以下に記述される内容は、後に生産されるワードの動作を定義することになる。DOES>以下の内容のコードが実行されるときには、スタックのトップに、いつも、前にCREATEしたデータ域の先頭アドレスが置かれているということが前提される。このアドレスを経由してデータを利用、操作できるのである。
単純な例
簡単なコード例で説明する。
: ACCUMULATOR ( "<spaces>name" n -- ) CREATE , DOES> tuck +! @ ;
このACCUMULATORという語を宣言的に用いることで、固有データを持つワードが生産されるのである。
このワードが実行されたとき、CREATEによって、名前がパースされ、現在の空きデータ域(データディクショナリ)の開始点(HERE)のアドレスとが結びつけられる。次の , でスタックから値が取られ、4バイトデータ領域に、その値が格納される。これはACCUMULATOR宣言のときに実行されるので、その際に定義されるワードの、いわば、固有データ域の初期値になる。
そうした後で、DOES>以下、; までの内容の実行コードもまた、CREATEされたワードに結びつけられる。しかし、結びつけられるだけである。DOES>以下の実行内容は、ワードACCUMULATORの実行時には実行されないのである。この部分は、ACCUMULATORで生産されたワードの実行時に初めて実行される。
DOES>以下の内容の実行時、スタックの一番上には、CREATEで結びつけられたデータ域のアドレスがある。そこに格納されているデータに、スタックからの入力値を加算した上で取り出す、というのが上のコード内容である。
利用例:
1 ACCUMULATOR accum \ 初期値1でワードaccumを定義
2 accum \ -- 3 初期値に2を加算し、結果を取り出して、スタックに置く
drop \ とりあえず、不要な値を捨てる
5 accum \ -- 8 現在値に5を加算して…以下同じ…
drop
243 accum \ -- 251
...
という具合である。ここでのaccumのようなワードは初期値をスタックに置いた形のACCUMULATOR宣言でいくつでも生産できる。それぞれがそれぞれのデータ域を持つのである。普通に他のワードから呼び出して利用することももちろんできる。
内部的規格
CREATE-DOES>を用いてワードを定義する場合、つまり上の例でいうと、ACCUMULATORの定義においては、このコロン定義は通常のワード定義として開始されている。したがって、初めのうちはACCUMULATORというワードの内容を定義しているのである。しかし、それはDOES>で終わる。つまり、DOES>は、ACCUMULATORの定義をいったん終了させて、新たに無名ワードの定義を開始するのである。そして、その内容は ;で終了する。
ACCUMULATORが実行されたときには、その内容、つまり、DOES>の直前までが、まずは実行される。このときに、CREATEが実行されて、新たなデータ型ワードaccumが定義される。そのとき、それに続いて、DOES>もまた実行されるのである。DOES>の実行内容は、「もっとも最近に定義されたCREATE型のワードに、DOES>以下の無名ワードの内容を、その実行内容として付け足す」というのである。
このような理屈付けで、accumの実行内容は、まずCREATEされたワードの実行内容として、それに結びつけられたデータ域のアドレスを返し、続いて、DOES>以下に記述されたコード内容を実行する、ということになるのである。
このような規格記述からみると、DOES>が一つの定義内に二つ以上あるのは意味がない。それらは順次実行されるが、直前にCREATEされたワードは一つしかないからである。効果は規格としては不定だが、前のDOES>の内容が、後のDOES>の内容で上書きされてしまうことになりそうである。他方、CREATEはDOES>の前に何個もあってもよいが、最後のCREATEに結びついた名前だけに、DOES>以下のコードの実行が付帯される、ということになる。逆に、一つのワード定義内で、DOES>の前に必ずCREATEがなければならないわけではない。DOES>が実行される「直前」の定義であればよいのである。ただし、直前の定義がCREATEを用いたものでなければ効果は保証されない、と標準規格はいうにすぎない。したがって、CREATE関連ワードを含まず、DOES>と実行コードのみのワードを定義しても、それを実行する直前にVARIABLE等を定義すれば、その動作を変更するのである。例えば、
: VARIABLE>VALUE DOES> @ ;
VARIABLE VAR1
10 VAR1 !
VARIABLE>VALUE
VAR1 \ -- 10
のように、VARIABLEであるから本来はそのアドレスを返すはずのVAR1の動作に@が追加されて、格納数値そのものが出力されるように変更されるのである。(もっとも、これだとVAR1に値を格納するのが面倒になるだけだが。)
ところで、DOES>の前と後は別個のワード定義として切断されており、それぞれがワード定義としての実質を持っている。したがって、もしも局所変数が利用できる環境であれば、規格上は、DOES>の前部分と後部分の双方に、それぞれ局所変数を利用できるということになる。Mopsで書けば、(標準forthなら局所変数は{: :}の対を使えばよい)
: NON-SENSE { in1 in2 -- } CREATE in1 in2 max , DOES> { p1 p2 -- n } p2 @ p1 or dup p2 ! ;
のようなコードも動作する。この例では、in1 in2 はNON-SENSEを実行する際に用いられる局所変数、p1 p2はNON-SENSEで生成されたワードを実行する際に用いられる局所変数、ということになる。ちなみに、DOES>以下のパラメターのトップは、必ずCREATEされたデータ域のアドレスであるから、p2はそのアドレス値で初期化されることになる。
少し込み入った例
プライベートデータと結合されたサブルーチンという発想は、オブジェクト指向を思わせる。実際、素朴ではあるがクラス定義とインスタンス生成のようなことが、このCREATE-DOES>を用いて実現できるのである。
そこで、まず、できるだけ標準forthに沿って(やや皮肉に)、文字列クラスを定義してみる。
0 value BASE|
1 value PUT|
2 value GET|
3 value RESIZE|
4 value APPEND|
5 value $APPEND|
6 value PRINT|
7 value RELEASE|
defer doput_
defer doget_
defer doresize_
defer doappend_
defer doprint_
: STR
CREATE 0 , 0 ,
DOES> swap
CASE
BASE| OF ENDOF
PUT| OF doput_ ENDOF
GET| OF doget_ ENDOF
APPEND| OF doappend_ ENDOF
$APPEND| OF swap doget_ rot doappend_ ENDOF
PRINT| OF doprint_ ENFOF
RELEASE| OF dup @ FREE drop 2 cells erase ENDOF
." Unknown Method" cr
ENDCASE
;
\ implement methods
:noname ( addr len obase -- ) >R dup ALLOCATE drop r@ ! dup r@ cell+ ! r> @ swap cmove ; is doput_
:noname dup @ swap cell+ @ ; is doget_
:noname dup >R @ over resize drop R@ ! R> cell+ ! ; is doresize_
:noname ( addr len obase -- ) 2dup dup cell+ @ rot + swap doresize_ 2dup cell+ @ swap - swap @ + swap cmove ; is doappend_
:noname doget_ type ; is doprint_
STR S1
STR S2
スタック操作がやや過剰(特にdoAppend_の内容)で、読みにくいが、規格通りのforth(ただしメモリーワードオプションを使った)なら動作するはずである。例えば、
" It's a simple string object! " PUT| S1
PRINT| S1
It's a simple string object! <- 印字される
" Not very difficult to implement because of Forth!" PUT| S2
PRINT| S2
Not very difficult to implement because of Forth! <- 印字される
BASE| S2 $APPEND| S1
PRINT| S1
It's a simple string object! Not very difficult to implement because of Forth! <- 印字される
RELEASE| S1 RELEASE| S2 \ 全内容を消去してヒープ部分を解放
というように動作する。
これはサブクラスは定義できないし、メソッド選択もあからさまな条件分岐になっているが、データとメソッドの結合という点で、簡易なオブジェクト指向機能は果たしうることが分るであろう。
標準Forthにおいて、十数行程度でオブジェクト指向システムを実装するという簡易なOOシステムが有名であり、確かそれも継承機能はなかったと思うが、実際のところはforthの簡易オブジェクト指向システムは、実装用コード0行でできるのである。
コード面はほとんど変わらないが、次のようにiMopsを用いれば、実装としてもう少し「よりオブジェクト指向的」なコードが書ける。メモリーワードオプションは定義されていないが、同等のC標準ライブラリーコールが利用できる。もちろん、本体にクラスが定義されているのであるから、実際には無益なコードだが。
TYPE{
BASE|
PUT|
GET|
RESIZE|
APPEND|
$APPEND|
PRINT|
RELEASE|
}
defer doput_
defer doget_
defer doresize_
defer doppend_
defer doprint_
: STR
CREATE
12 reserve
DOES> { this -- }
SELECT[ BASE| ]=> this
[ PUT| ]=> this doput_
[ GET| ]=> this doget_
[ RESIZE| ]=> this doresize_
[ APPEND| ]=> this doappend_
[ $APPEND| ]=> doget_ this doappend_
[ PRINT| ]=> this doprint_
[ RELEASE| ]=> this Z@ free this 12 erase
DEFAULT=> drop ." Unknown Method" cr
]SELECT
;
\ implement methods
:noname { addr len this -- } len malloc this Z! len this cell+ ! addr this Z@ len cmove ; is doput_
:noname ( this -- ) dup Z@ swap cell+ @ ; is doget_
:noname ( size this -- ) dup >R Z@ over resize drop R@ Z! R> cell+ ! ; is doresize_
:noname { addr len this -- } this cell+ @ len + this doresize_ addr this cell+ @ len - this Z@ + len cmove ; is doappend_
:noname doget_ type ; is doprint_
STR S3
ここで用いたSELECT[ ]SELECTは、機能的にはCASE構造とほぼ同じだが、内部の仕組みは実はジャンプテーブルである。そして、メソッドの符牒として用いられる数値(これは定数として定義される)は、ジャンプテーブルへのオフセットに対応している。つまり、このメソッドセレクターはvtableによってバインドされているのである。効率としても、定義されるメソッドの個数が増えても実行速度には直接は関連がない(サイズの問題はある)。
あまり意味はないが、ワード名を変えれば、次のように、STRクラス定義の部分を、なんとなく、もっとそれっぽくもできる(シンタクティック・シュガーなどという)。
: IVAR-FIELD CREATE ;
: END-IVARS postpone DOES> ; immediate
: METHODS[ postpone SELECT[ ; immediate
: END-METHODS postpone ]SELECT ; immediate
: STR
IVAR-FIELD
12 reserve
END-IVARS
{ this -- }
METHODS[ BASE| ]=> this
[ PUT| ]=> this doput_
[ GET| ]=> this doget_
[ RESIZE| ]=> this doresize_
[ APPEND| ]=> this doappend_
[ $APPEND| ]=> doget_ this doappend_
[ PRINT| ]=> this doprint_
[ RELEASE| ]=> this Z@ free this 12 erase
DEFAULT=> drop ." Unknown Method" cr
END-METHODS
;
Localsを宣言して、METHODS[ の手前でthisからの適当なオフセットを加算して準備しておけば、インスタンス変数(ただし、ポインター)の名前としても利用することができるであろう。
継承機構も付けられそうな気がしてきた(ivarフィールドの加算とデフォルトからのメソッドテーブル分岐で良いわけである)が、内部実装に依存したものになるのでヤメておこう。
固有データ領域へのアクセス
CREATE-DOES>ワードで生成されたワードは固有データ領域を持つが、そのデータ領域は原則としてDOES>部分でのみアクセス可能であり、外部から直接には見えない。
しかし、全く知ることができないのも不便である。そこで、これを可能にするワードが>BODYである。
>BODY ( xt -- addr )
ワード>BODYは、データ域を持つワードのxtをパラメターとして取って、それに結びついた固有データ領域のメモリーアドレスを返す。したがって、例えば、最初の単純な例を用いて、
' accum >BODY
とすれば、accumの固有データ領域のアドレスがわかる。>BODYはCREATEされたワードについては全て利用可能であるので、VALUE変数のデータ格納領域を知るためにも利用できる。
次は、
最終更新:2019年06月15日 13:36