packet型の導入
nmoshのexpanderを自前のものに置き換える重要な理由は静的型の導入にある。これは僕が修論で作ったyuniSchemeに由来していて、構文だけが新たに設計されている。
(要するに、nmoshにdefine-packet-formatのようなyuniSchemeのconstructを導入することになる。)
型としてのpacket
packetは物理フォーマットが外部制約によって決定されている。この性質を利用してyuniSchemeでは、packet typeそのものを静的型として利用する。
(define-dict schemeTag ... (1 pair) (2 fixnum) ...) (define-packet-format schemeTag (/ (* 4 bits))) (define-packet-format pair (tag (= schemeTag pair)) (car schemeObject) (cdr schemeObject)) (~define (car pair) (~ pair 'car))
例えば、pairもcarやcdrの各フィールドを持つpacketとして考える。この例では、pair型としてtag car cdrの3つのフィールドを持つパケットを定義し、car手続きとしてpair型のパケットからcarフィールドを取り出す手続きを定義している。
...極論を言えば、コンピュータそのものが静的型付けのマシンとなっていると言える。動的型付けはソフトウェア的に実現できる。
~ prefixの意味
この定義におけるパケットを取り扱う構文や手続きは全て~でprefixする。
単純に ~ とだけ書いた場合はuniversal accessor or setterとして働く( http://d.hatena.ne.jp/mjt/20100723/p1 )。
多分、~defineや~letのような構文を用意する必要が有るだろう。
これらの~ prefixを持つ構文は、type annotationをバックエンドに対して付与するために用意される。
原理的には、ちゃんとしたコンパイラはannotationが無くても適切にaccessorやsetterをインライン化できる。しかし、コンパイラとしての要求としてリアルタイム性も求められているので、適切なannotation手段を持つことも必要になってくる。
type aware 手続きと~define
上の例で使っている~defineはtype aware手続きを定義する。type aware手続きは、packetを処理するための専用の手続きで、以下の特徴を提供する。
- 型チェックを自動的に行う。例えば、引数に対して、pair?やlist?を明示的に呼ぶ必要は無い。
- 型名を仮引数として使うことができる。
後者のメリットは微妙なところだが、仮引数名を省略することで本来のScheme手続きと見た目を近くできる。上の例のcarは、この特徴を使わない場合次のように書ける。(yuniはgoのように型を後置することに注意)
(~define (car (p pair)) (~ p 'car))
前者の特徴はいわゆる型推論で実現される。この推論はtype aware手続きから別のtype aware手続きを呼んだ場合にだけ行われる。このため、~mapのようなSRFI-1のtype awareなバリアントが提供されると同時に、非type awareな手続きを通すことでこの推論を無効化することができる。
これは、適切なエラー報告を提供するために利用できる。
上のcarは、よりsexyなエラーを報告するために次のように改良できる。
(~define (car pair) (unless (pair? pair) (assertion-violation 'car "pair needed" pair)) (~ pair 'car))
assertion-violationはtype aware手続きで無いため、このcar手続きでは暗黙の型推論は行われなくなる。
ちゃんとしたコンパイラが書かれれば、type awareでない手続きを分割してtype awareな手続きを独立させることで最適化することができるだろう。上のケースで言えば、(assertion-violationの継続から帰ってこないと仮定することで、)unlessを通過した後のpairはpairオブジェクトであることが保証されていると考えられる。