port
とりあえず、平行プリミティブとしてportを使うことにする。
これはpi計算や他のプロセス代数によく似ている。
一般化port
いわゆるR6RSのportはbinaryかtextしか扱えないため、一般化したportを考える。
これは要するに遅延評価されるclosureで、原理的にはclosureで実現できるはずだが、portは通常のclosureと以下の点が異なる :
- 動作をキャンセルできる
- multiplexできる(後述)
- タイムアウトできる
このシステムをデザインする上で最も難易度が高いのはportを通るデータの値域を限定することにある。値域を限定しなければ、異なるVM間でデータをやりとりすることができなくなってしまう。
(このようなportは素のSchemeでも原理的には実装できる。単に実装効率のために独立してデザインしている。)
portfactory
一般化portを返すclosureをportfactoryと呼ぶ。例 :
- ネットワークインターフェース
- ファイルシステム
- multiplexer
- 一般的な式
例えば一般的な式をasync-evalに渡すとportを得る(もちろん現段階ではasync-evalなんてものは無い)。
非同期I/Oのために、port自身もportfactoryとなる。つまり、(async-read port) => portであり、返されたportをreadすることで初めて完了が保証される。
portに対する操作は大量に存在する。read, write, seek, tell, checkpoint, ...。R6RSのcustom portのような方式はすぐに破綻するだろう。
multiplexer
multiplexとは、複数のportを同時に待つ事を言う。multiplexerをportfactoryにするかどうかは微妙な問題といえる。
要するに、
(cond-parallel ((read socketA) ...) ((read socketB) ...) ...)
のようなプリミティブを用意するべきなのか、プリミティブで待てるオブジェクトは1つに限定すべきなのか。
例えば、FreeBSDのkqueueはportfactoryとよく対応している。multiplexer portfactoryを導入することで、待つたびにkqueueを作成/破棄する必要が有るよりは、明示的にkqueueを作成できる方が好ましいように思える。libeventのインターフェースもこちらになっている。
しかし、select()のように毎回何を待つか指示しなければならないようなケースではオーバーヘッドに違いは無い。
実装
いろいろ考えたが、今のところは、1threadにつき1portで行くことにした*1。
libeventだと待てるオブジェクトと待てないオブジェクトが出てくる。例えばSSLライブラリを外部のライブラリを使って実装した場合、それがSSLストリームをfdに見せてくれるとは限らない気がする。もちろん、そのような場合でも専用のthreadを用意してpipeにwriteすることでfdに見せることが出来るが。。
あとlibeventはWindowsにおいてSocket以外を扱うのに不安がある。確かに今やWinSockのsocketはReadFileといった通常のI/Oルーチンで扱えるが、その逆、つまりファイルハンドルをWinSockのselect()で扱うのが裏技でない根拠を知らない。
futureやspawnのようなプリミティブがscheme(やLisp)では一般的につかわれるが、これらはpersistentでない つまり、一旦値を返すと消滅するので、直接的に実現すると再生成の操作が必ず必要になる*2。このため、より"ハードウェアに近い"portをプリミティブにし、futureやspawnはportを使って(静的にコードを生成して)実現することを考える。
R6RSのportと異なり、このportは一方通行になる。