前回は CREATE と ALLOT を使って、メモリ上に配列を自作する方法を学んだ。
FORTHの真価はここからである。単に「データを置く」だけでなく、データに固有のふるまい(動作)を与えることができる。
その中心となる仕組みが DOES>である。DOES> は、「CREATEで作ったワードが実行されたとき、何をするか」を指定する。これにより、FORTHでは“データと動作を一体化したオブジェクト”を定義できる。
CREATEの復習 ― 名前と場所を作る
まず、CREATE の基本を思い出しておこう。
CREATE BUF 100 ALLOT
これで BUF という名前が辞書に登録され、100バイト分の領域が確保される。
BUF を実行すると、そのアドレスがスタックに積まれる:
BUF .
1904536
FORTHでは、これが「データオブジェクトの雛形」となる。次の DOES> は、このBUFのような“データを持つ存在”に動作を付ける。
DOES> ― オブジェクトに動作を与える
: <定義名>
CREATE ... (定義時の処理) ...
DOES> ... (実行時の処理) ...
;
CREATE が“構造”を作り、
DOES> が“ふるまい”を定義する。
DOES> を実行すると、直前に CREATE されたワードの「実行動作」を、DOES> 以降の部分に置き換える。つまり、CREATEしたワードが別の動作を持つようになる。
配列を作るワードを作る ― 「クラス的構造」
Lev 11 で作った ARRAY: は、その典型例である。
: ARRAY: ( n -- )
CREATE CELLS ALLOT
DOES> ( i -- addr ) SWAP CELLS + ;
定義の動き
-
定義時 (
ARRAY:実行時):-
CREATEにより新しいワード(例:DATA)を登録 - 渡されたサイズ(
n)セルぶんのメモリを確保
-
-
実行時 (
DATA実行時):-
DOES>以降の動作が呼び出される - スタックのインデックス値から要素のアドレスを計算して返す
-
使用例
10 ARRAY: DATA
100 0 DATA !
200 1 DATA !
300 2 DATA !
0 DATA @ . 1 DATA @ . 2 DATA @ .
出力:
100 200 300
DATA は、10要素の配列という「オブジェクト」 になっている。
ARRAY: は、「配列を作るクラス」 のようなものだ。
DOES>の内部動作 ― 実行部を書き換える
FORTH処理系は、DOES>を実行すると次のようなことをする。
-
CREATEによって登録されたワードには、内部的に「コードフィールド(CF)」がある。 - 通常、このCFは
DO CREATEを指し、実行時にはアドレスを返す。 -
DOES>はこのCFを書き換え、以後そのワードを実行するとDOES>部のコードが呼ばれる。
言い換えれば、DOES> は“コンパイラ内部を書き換える命令” である。FORTHの「言語を自分で拡張できる」力は、ここにある。
DOES> は 1 つの CREATE ... DOES> に対して 1 回しか書けない
定数とカウンタを自作する ― 「ふるまい付きデータ」
(1) 定数を作る
: CONST: ( n -- )
CREATE , \ 値を辞書に書き込む
DOES> ( -- n ) @ ; \ 実行時に読み出して返す
使用例:
100 CONST: SPEED
SPEED .
出力:
100
ここで CONST: は、DOES> によって 「値を返すオブジェクト」、まさに 定数 を自作している。
(2) カウンタを作る
: COUNTER: ( -- )
CREATE 0 , \ 初期値0を記録
DOES> ( -- )
1 OVER +! \ 値を1増やす
@ . ; \ 現在値を表示
COUNTER: C1
COUNTER: C2
C1 C1 C1
C2 C2
1 2 3
1 2
それぞれ独立したカウンタとして動作している。これがFORTHにおける「インスタンス」である。
データと動作を統合する ― オブジェクト的思考
FORTHでは、CREATEが「データ構造(メモリ領域)」、DOES>が「動作(メソッド)」を担う。
この仕組みを使うと、複数のデータオブジェクトに共通の動作を与えたり、異なるふるまいを持つ構造を同時に管理できる。
\ 保存領域作成
: DATA: ( n -- addr )
CREATE CELLS ALLOT ;
\ 書き込み専用ワード(配列オブジェクトを利用)
: STORAGE: ( 'array -- )
CREATE , \ 配列のアドレスを記録
DOES> ( value i -- )
@ SWAP CELLS + ! ;
\ 読み出し専用ワード(同じ配列を利用)
: DISPLAY: ( 'array -- )
CREATE , \ 配列のアドレスを記録
DOES> ( i -- )
@ SWAP CELLS + @ . ;
5 DATA: ARR
ARR STORAGE: BOX
ARR DISPLAY: VIEW
123 2 BOX \ ARR[2] = 123
456 4 BOX \ ARR[4] = 456
2 VIEW \ → 123
4 VIEW \ → 456
ここでは、ARR に書き込む BOX と、参照する VIEW のオブジェクトが作成されている。
STORAGE:
-
与えられた配列のアドレスを自分の中に記録(
,で辞書に書き込む)。 -
実行時に:
-
@でその記録された配列アドレスを取り出す。 -
SWAPで順番を (value i) → (i value) に並べ替える。 -
CELLS +で i番目の要素のアドレスへ進む。 -
!で値を書き込む。
-
DISPLAY:
-
STORAGE:と同じく、共有配列のアドレスを記録。 -
実行時に:
-
@で配列アドレスを取り出す。 -
SWAPで (i addr) の順に整える。 -
CELLS +で該当要素へ。 -
@で値を読み出し、.で表示。
-
複合構造 ― 配列+関数の統合
FORTHでは、データと動作を複合化することもできる。以下は「データを持ち、平均を計算する構造体的オブジェクト」の例である。
\ --- データを使って平均を計算するオブジェクトを作る ---
: STAT: ( addr -- )
CREATE CELLS ALLOT
DOES> ( i addr -- n )
0
2 PICK 0 DO
OVER I CELLS + @ + ( addr i )
LOOP
ROT / SWAP DROP ;
\ 5つの配列を作成
5 STAT: DATA
\ データを登録
10 0 CELLS ' DATA + !
20 1 CELLS ' DATA + !
30 2 CELLS ' DATA + !
40 3 CELLS ' DATA + !
50 4 CELLS ' DATA + !
\ データを表示
0 CELLS ' DATA + @ .
1 CELLS ' DATA + @ .
2 CELLS ' DATA + @ .
3 CELLS ' DATA + @ .
4 CELLS ' DATA + @ .
\ 5つのデータの平均を計算
5 DATA .
30
' DATA は、DATA のアドレスを取り出している。
このように、DATA は「5つの値を持ち、呼ばれると平均を返す」。まさにデータと動作が一体化したオブジェクトである。
DOES> の応用 ― 言語拡張の原型
FORTHの標準ワードの多くは、実は DOES> を使って再定義できる。
たとえば VARIABLE は次のように書ける:
: VARIABLE
CREATE 1 CELLS ALLOT
DOES> ;
DOES> 部が空なのは、VARIABLE の実行時動作が「アドレスを返す」だけだからである。FORTHの文法要素が、すべてユーザー定義で再構築できるというのは、他の言語にはない柔軟性である。
まとめ ― DOES>が拓く“構造と思考の一致”
| 構文要素 | 意味 |
|---|---|
CREATE |
名前とメモリ領域を作る(構造) |
DOES> |
その構造のふるまいを定義する(動作) |
, |
定義時に値を書き込む |
@ / !
|
メモリからの読み出し/書き込み |
CELLS |
セル単位のアドレス計算 |
FORTHでは、CREATE … DOES> によって「クラス → インスタンス」「構造 → 動作」の関係をすべて自作できる。この仕組みは、C言語の構造体でも、Lispのオブジェクトでもなく、FORTHそのものが“メタ言語”として動作していることを示している。
演習課題
-
CREATE … DOES>を使って「1次元ベクトル」を定義し、- 要素設定 (
SET-VEC) - 要素取得 (
GET-VEC) - スカラー倍 (
SCALE-VEC)
を実装せよ。
- 要素設定 (
-
CONST:を改良し、定義時にラベル文字列を保存して、
実行時に「名前: 値」を表示するようにせよ。 -
複数の
COUNTER:を生成し、それぞれが独立して動作することを確認せよ。