channel-chatterではすでにストリームを使い、boatswains-bilgeではその始末をしました。この演習では、初めて自分でストリームを作ります。プロトコルを実装したクラスを作るので、<string-reader>で動いていたwith-input-stream、read1、stream-read1のコードが、そのまま自分のクラスでも動きます。
dragons-descendantsでTUPLE: child < parent …をすでに学びました。これは、スロットが親から子へと流れていく単一親の階層です。Factorには、クラスグラフを拡張するもうひとつの方法があります。オープンミックスインです。MIXIN: mはスロットを持たないクラスを宣言し、他のクラスは後からINSTANCE: c mでそこに参加できます。所属は多対多で、ひとつのクラスは好きなだけ多くのミックスインのインスタンスになれます。
ストリームのプロトコルでは、2つのミックスインを使って、クラスがインターフェースのどちら側を実装しているかを示します。
INSTANCE: my-stream input-stream ! readable
INSTANCE: my-stream output-stream ! writable
ストリームは、どちらか一方でも、両方でもかまいません。input-stream(またはoutput-stream)のジェネリックメソッドは、そのミックスインに参加した任意のクラスへディスパッチされます。周囲からそのまま使えるread1、stream-contents、with-input-streamなどが自分のクラスにたどり着くのは、こういう仕組みです。
ミックスインはオープンです。ミックスインを宣言したあとでも、たとえ別のボキャブラリーであっても、新しいメンバーを追加できます。これがTUPLE: < parentとの違いです。こちらは子クラスを宣言した時点で関係が固定されます。
プロトコルのジェネリックはioにあります。
stream-read1 ( stream -- elt/f )
stream-read ( n stream -- seq/f )
stream-read-unsafe ( n buf stream -- count )
stream-write1 ( elt stream -- )
stream-write ( seq stream -- )
stream-flush ( stream -- )
stream-element-type ( stream -- type )
stream-read1は次のバイトを返します(ストリームの終端ではf)。stream-write1は1バイトを書き込みます。stream-read-unsafeは、ほとんどの入力ストリームが提供する低レベルの一括読み込みです。stream-readはその上に作られていて、メモリの割り当てを扱います。stream-element-typeは、そのストリームがバイト(+byte+)を扱うのか文字(+character+)を扱うのかを表します。別のストリームをラップする場合、いちばん簡単な実装は、その下にあるストリームにそのまま委譲するだけです。
destructorsからストリームはディスポーザブルです。自作のストリームはdisposableを継承し、dispose*を実装します。これはboatswains-bilgeで扱ったフレームワークを、ストリームのクラスに当てはめたものです。
TUPLE: my-stream < disposable underlying ;
: <my-stream> ( underlying -- s )
my-stream new-disposable swap >>underlying ;
M: my-stream dispose* underlying>> dispose ;
これさえ整えば、channel-chatterで出てきたwith-…系のコンビネーターが、自分のストリームでも自動的に動きます。
貨物船で無線通信士をしています。信号は雑音だらけの回線を通して届きます。船の機械の音、天候による空電、ときどき混ざる外国語の通信。それらに紛れて、本来聞き取りたいモールス信号が流れてきます。メッセージを捉えるには、回線の信号をテープ装置に通します。この装置は、有効なモールス符号(.、-、および文字と文字の区切りとなる空白)以外をすべて取り除いてくれます。
ここでの課題は、このテープをFactorのカスタム入力ストリームとして組み立てることです。
disposableを継承し、基になる入力ストリームを保持するスロットwireを1つ持つtapeタプルを定義しましょう。
TUPLE: tape < disposable wire ;
次に、コンストラクター<tape> ( wire -- tape )を書きましょう。デストラクターの仕組みがテープを登録できるよう、newではなくnew-disposableを使い(newではありません)、基になるストリームはwireスロットに保存しましょう。
tapeをinput-streamとして宣言しましょう。
INSTANCE: tape input-stream
こうすると、プロトコルの既定のコンビネーターが、定義したクラスにディスパッチされるようになります。
M: tape stream-read1 ( tape -- elt/f )を実装しましょう。基になるwireからバイトを読み取り、CHAR: .、CHAR: -、CHAR: \s(空白)のいずれかに当たるまで進めます。そのバイトを返します。wireがf(ストリームの終端)を返したら、こちらもfを返します。テープの役割はそこで終わりです。
"x.y -ab cz" <string-reader> <tape>
[ stream-read1 ] [ stream-read1 ] [ stream-read1 ] tri
! => CHAR: . CHAR: - CHAR: \s
M: tape stream-element-type ( tape -- type )を実装し、wireに委譲しましょう。テープは、読み取る要素の種類を変えません。
M: tape dispose* ( tape -- )を実装しましょう。フレームワークは、disposeが呼ばれるたびに、定義したメソッドをちょうど1回呼び出します。あとは基になるwireを破棄するだけです。
この5つの部品がそろえば、テープは通常の入力ストリームとして機能します。with-input-streamやstream-readをはじめ、ほかのプロトコルのワードはすべて、定義したメソッドに自動的にディスパッチされます。