補遺: 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 (4)

4 -1 rollクロージャとして実行したい実行可能配列を先頭にもってきます。

{counter 1 add}
begin
--dict--
--mark--
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}

つまり、もともとの実行可能配列に カレント辞書, beginend でラッピングしていることになっていますね。