補遺: PostScript の cvx とクロージャ
PostScript でもクロージャはできるよ、というお話。以下のサイトを参考にしました。
一般的な LL 言語などを模倣してクロージャぽくカウンタを書いてみます。
/make-counter { << /counter 0 >> begin { /counter counter 1 add def counter } end } def
ところが実行すると、
/counter1 make-counter def counter1 % => Error: /undefined in counter % Operand stack: % counter % Execution stack: % (略)
このようにエラーになってしまいます。
これは、実行可能配列で指定されている counter
というトークンが実行時に評価されるんだけれど、実際には make-counter
の処理が終わったあとではその当時の辞書が end
によって消失してしまっているから、現在の辞書群に対して counter
を探索して、ありませんっ!て怒られているんですね。
これじゃ PostScript でクロージャを(普通に)実現するのは無理だよ、と思って調べていたら、
x内で作った無名辞書は、通常、xを抜けると参照できなくなってしまいます。 手続きclosureでは、PostScriptの「リテラル配列を実行可能配列に変更できる」という特性を使い、実行可能配列の中に、その無名辞書への参照を残しています。
歪 : PostScriptでクロージャ
という表記がありました。
cvx
オペレータとは
cvx
オペレータは、任意の引数を実行可能オブジェクトに変換します。
たとえば、名前リテラルにたいして。
1 2 add % add というトークンを積もうとした瞬間に評価される pstack % => 3 clear 1 2 /add cvx % cvx で名前等を実行オブジェクトに変換できる pstack % => add % ! % 2 % 1 exec == % exec でスタックトップの実行可能オブジェクトを実行する % => 3
配列に対して実行すると、実行可能配列に変換します。
[ 1 2 /add cvx ] pstack % => [1 2 add] % このままではただの配列 cvx % cvx で変換すると pstack % => {1 2 add} % 実行可能配列になった exec == % さきほどと同様 exec で実行できる % => 3
これらはまるで一般的な LL 言語での eval みたいですね。
まずは実直?なカウンタ
まずはクロージャとか考えずに、インスタンス指向?でカウンタを書いてみます。おのおのの無名辞書をインスタンスとみなす、みたいな。
/make-counter { << /counter 0 >> dup /step 4 -1 roll put % さきほどの無名辞書に /step というキーで引数を登録 % この時点でさきほどの無名辞書がスタックに残っている => 戻り値 } def /retrieve-counter { % スタックトップの辞書に対して begin counter step add /counter exch def counter end } def /counter1 1 make-counter def /counter2 3 make-counter def counter1 retrieve-counter == % => 1 counter2 retrieve-counter == % => 3 counter1 retrieve-counter == % => 2 counter2 retrieve-counter == % => 6 counter1 retrieve-counter == % => 3 counter2 retrieve-counter == % => 9
retrieve-counter
という手続きを実行しないといけないですが、とりあえずカウンタは実現できました。
cvx
を使ってクロージャを実現する
さきほどの cvx
と配列をくみあわせて、クロージャを使って書いてみます。
/make-counter { << /counter 0 >> dup /step 4 -1 roll put % この時点でさきほどの無名辞書がスタックに残っている [ 2 1 roll % 無名辞書を配列の内側にもってくる % 配列の定義中はトークンは評価されてしまうので % 名前(シンボル的もの) を cvx で変換して命令列を push していく /begin cvx /counter cvx /step cvx /add cvx /counter /exch cvx /def cvx /counter cvx /end cvx ] cvx % これでこの配列が実行可能配列(無名関数的)に変換された => 戻り値 } def
ちょっと難しいですね。make-counter
を実行すると、
{--dict-- begin counter step add /counter exch def counter end}
のような実行可能配列が返ってきます。make-counter
内部で生成した無名辞書が、この実行可能配列の内側に入っている(温存されている)のがおおきいのです。
実行してみます。
/counter1 1 make-counter def /counter2 3 make-counter def counter1 == % => 1 counter2 == % => 3 counter1 == % => 2 counter2 == % => 6 counter1 == % => 3 counter2 == % => 9
きちんと独立してカウントできました。
でも、クロージャ部分に /
と cvx
が頻出してうざいですね。
実行可能配列の記述時には内部のトークンは評価されないこと、実行可能配列を aload
オペレータでスタックの要素に変換できることを応用すると、実はこのように書くことができます。
[ 2 1 roll { % 実行可能配列を構築する間はトークンは評価されない begin counter step add /counter exch def counter end } aload pop % スタックの要素群へ変換 ] cvx
中括弧で囲まれた部分は、実際におこなう処理そのままですね。ということは、このことを応用すると汎用的なクロージャ生成手続きを作れそうです。
汎用的なクロージャ生成
というわけで以下がクロージャ生成部分を汎用化したもの。といっても 歪 : PostScriptでクロージャ のコピペです。
/closure { [ currentdict /begin cvx 4 -1 roll aload pop /end cvx ] cvx } def /make-counter { 2 dict begin /step exch def /counter 0 def { counter step add /counter exch def counter } closure end } def
さきほどまで無名辞書を生成して dup
したりくねくねしてましたけど、ずいぶんわかりやすいコードを書けるようになります。
クロージャ生成ルーチンの挙動を追う
まぁほとんど再確認なんですが、最初上記の記事を読んだときに暗号にしかみえなかったのでクロージャ生成ルーチンの動作を追ってみました。
さきほどまでのカウンタだとコードがちょっと複雑なので、
{ 1 dict begin /counter 0 def { counter 1 add } closure end }
こんなシンプルなクロージャ生成の場合です(カウンタとしての用はなしませんが)。
クロージャ生成手続きの各シーケンスに番号を振りました。
/closure { [ % -- (1) currentdict % -- (2) /begin cvx % -- (3) 4 -1 roll % -- (4) aload pop % -- (5) /end cvx % -- (6) ] cvx % -- (7) } def
以下、各ステップでスタックがどのようになっていくのかの図示です。
step (1)
--mark-- {counter 1 add}
step (2)
currentdict
オペレータにより、現在の辞書をスタックトップに push しています。
--dict-- --mark-- {counter 1 add}
step (3)
begin --dict-- --mark-- {counter 1 add}
step (5)
aload pop
で渡された実行可能配列を分解してスタックにつみます。
add 1 counter begin --dict-- --mark--
step (6)
end add 1 counter begin --dict-- --mark--
step (7)
最後に cvt
をかけて実行可能配列に戻します。
{--dict-- begin counter 1 add end}
つまり、もともとの実行可能配列に カレント辞書, begin
と end
でラッピングしていることになっていますね。