For Development HEAD DRAFTSearch (procedure/syntax/module):

6.16 パラメータと動的状態

「パラメータ」はSchemeで動的スコープを持つ変数を実現します。 もともとSRFI-39で仕様が定められ、R7RSからは言語の一部になりました。

註: 0.9.13より、パラメータのセマンティクスを従来のものからSRFI-226で定義される ものに切り替えつつあります。両者は微妙に振る舞いが異なります。 詳しくはSRFI-226パラメータへの移行で説明しています。

パラメータはmake-parameterで作られる特殊な手続きで、 ゼロ個または1個の引数を取ります。引数無しで呼ばれた場合は現在の動的環境での値を返し、 引数つきで呼び出された場合はその引数を現在の動的環境での値にセットして 以前の値を返します。

parameterizeマクロはletに構文的に似ていて、 本体の実行の間、パラメータの値を一時的に置き換えます。 これは字句的(静的スコープ)ではなく動的スコープで動作します。

(define var (make-parameter 0))

(define (show-var) (print (var)))

(show-var)  ; prints 0

(parameterize ((var 1))
  (show-var))    ; prints 1


(define f)

(parameterize ((var 2))
  (set! f (lambda () (show-var))))

(f) ; prints 0, since its out of the dynamic extent of var=2

パラメータの値はまた、新たな値を引数としてパラメータを呼ぶことで変更することができます。

(define var (make-parameter 0))

(var) ; ⇒ 0

(var 1)

(var) ; ⇒ 1

;; Gauche extension: you can use set! too
(set! (var) 2)

(var) ; ⇒ 2

元々、Schemeのパラメータは通常の手続きとdynamic-windを使って 動的束縛の変数をエミュレートするハックでした。 同じメカニズムはまた、動的な状態の管理にも使うことができました。

しかし使い込まれてゆくうちに、動的変数と動的状態は別に扱った方が良いという 知見が得られてきました。効率の良い実装が異なるのと、 限定継続があると振る舞いに差があるためです。 SRFI-226でこの違いが明確化されました。

Gaucheも0.9.13からこの区別を採り入れています。 詳しくはパラメータと動的状態の違いを参照してください。

これまで長くGaucheを使っていた方は、一度0.9.13での変更点と 移行方法をチェックしておいてください (SRFI-226パラメータへの移行参照)。


6.16.1 パラメータ

歴史的事情から、パラメータには、少しだけ振る舞いが違う3つの種類があります。 但し、その違いはマルチスレッドコードで破壊的変更をしない限りは見えません。 R7RSの範囲、すなわちパラメータを動的束縛にだけ使っている場合は、 どのパラメータも同じように振る舞います。

その3つの種類は、共有パラメータスレッドパラメータ、 そしてレガシーパラメータです。 最初のふたつはSRFI-226で定義されます。 (SRFI-226では「共有パラメータ」とは呼んでいませんが、make-parmaeterで 作られる通常のパラメータが共有パラメータになります)。 最後のものはGauche 0.9.12以前と互換なパラメータです。

次の表に、パラメータの破壊的変更が他のスレッドから見えるかどうかをまとめます。

共有スレッドレガシー
トップレベルの変更が見えるか?yesnono
動的範囲での変更が見えるか?yesnoyes

ここで「トップレベルの変更」とはparameterizeがされていない動的環境での 変更、「動的範囲での変更」とはparameterizeが有効な動的環境での変更です。

(define p (make-parameter 0))

(p 1) ; トップレベルの変更

(parameteize ((p 2))
  (p 3)  ; 動的範囲での変更
  )

(p 4) ; トップレベルの変更

共有パラメータとレガシーパラメータについては、 動的範囲での変更が見えるのは、同じ動的環境を共有しているスレッド間のみです。

Function: make-parameter value :optional converter
Function: make-shared-parameter value :optional converter
Function: make-thread-parameter value :optional converter
Function: make-legacy-parameter value :optional converter

[R7RS base][SRFI-226] 初期値がvalueであるパラメータを作成します。 もし省略可能な引数converterが与えられた場合、 それは一つの引数を取る手続きでなければなりません。 パラメータの値が変更されようとした時、converterは与えられた値を 引数として呼ばれ、converterが返した値がパラメータの新しい値と なります。converterはエラーを報告したりパラメータの値を変えずに置くことも 可能です。

上で説明したように(パラメータ参照)、パラメータには3種類あります。 make-shared-parametermake-thread-parametermake-legacy-parameterはそれぞれ 共有パラメータ、スレッドパラメータ、レガシーパラメータを作ります。

互換性のため、make-parameterはどのモジュールをuseするかで 動作が変わります。

  • gaucheモジュールのmake-parameterは、0.9.13では 互換性のためにmake-legacy-parameterと同じ動作ですが、 将来はmake-shared-parameterに切り替わります。 以前のコードを使いつづけるには、gauche.parameterモジュールをuseするか、 make-parametermake-legacy-parameterに置き換えてください。
  • gauche.parameterモジュールをuseすると、 make-parametermake-legacy-parameterと同じ動作になります。 これは今後も変わりません。gauche.parameterは互換性のためのモジュールとなります。
  • srfi.226をuseした場合、make-parameterはSRFI-226で 定義されているとおり、すなわちmake-shared-parameterの動作となります。

註: R7RSはmake-parameterを、 SRFI-226はmake-parametermake-thread-parameterを 定義しています。 他のふたつ、make-shared-parametermake-legacy-parameterはGauche独自の名前です。

註: 0.9.9までは、この手続きはパラメータオブジェクトを返していました。 パラメータオブジェクトは object-applyメソッドによって手続きであるかのように 振る舞います。しかし、R7RSはmake-parameterが手続きを返すように 規定しており、ポータブルなコードでは (procedure? (make-parameter 'z))#tでなければなりません。 0.9.10から、make-parameterは手続きを返すようになりました。 通常のパラメータの使い方をしている限り、外部に見える振る舞いは変わりません。 ただし、make-parameterで作られたオブジェクトかどうかを (is-a? p <parameter>)でテストしているコードは 後述するparameter?を使うように変更する必要があります。

Macro: parameterize ((param value) …) body …

[R7RS base][SRFI-226] body …を評価します。 但し、body … の実行中のみ、パラメータparamの値を valueに変更します。最後のbodyの返した値を返します。

parameterizeフォームが末尾位置にあり、 全てのparamがパラメータに評価された場合、 bodyは末尾位置で評価されます。 (これはR7RSでは要請されていませんが、SRFI-226で要請されます)。

R7RSとSRFI-226はparamがパラメータへと評価されることを要求しています。 Gaucheは伝統的に、paramに動的状態も許してきましたが、 そのような使い方は非推奨となりました。動的状態の切り替えにはtemporarilyを 使うようにしてください (動的状態参照)。 経過措置として、 Gauche 0.9.13では実行時に全てのparamがパラメータに評価されたかどうかを チェックし、パラメータ以外のものが混じっている場合は旧式の実装へと切り替えます (その場合、bodyは末尾位置では評価されません)。 将来のバージョンではこの措置は無くなります。現在のコードが将来に渡って使えるかどうか 確かめるにはコードに(use srfi.226)を加えてみてください。

例:

(define a (make-parameter 1))
(a) ⇒ 1
(a 2) ⇒ 1
(a) ⇒ 2
(parameterize ((a 3))
  (a)) ⇒ 3
(a) ⇒ 2
Function: parameter? obj

[SRFI-226] objmake-parameterで作られたものであった場合は#tを、 そうでなければ#fを返します。

Function: current-parameterization

[SRFI-226] 現在のパラメータの動的束縛の状態を具体化したパラメタライゼーションオブジェクト を返します。パラメタライゼーションcall-with-parameterizationに 渡すことで別の継続へと受け渡すことができます。

Function: parameterization? obj

[SRFI-226] objがパラメタライゼーションなら#tを、そうでなければ#fを返します。

Function: call-with-parameterization parameterization thunk

[SRFI-226] call-with-parameterizationの呼び出しの継続から パラメタライゼーションだけparameterizationと置き換えた継続のもとで thunkを呼び出します。 すなわち、この手続きが末尾コンテクストで呼ばれたなら、 thunkの呼び出しも末尾コンテクストになります。

Macro: parameterize/dynwind ((param value) …) body …

これはdynamic-windを使ってパラメータ束縛を管理するフォームです。 0.9.13より前のparameterizeと全く同じ動作をします。 このフォームは主に、レガシーコードを動かすために提供されています。

これは現在のparameterizeと、次の点が異なります。

  • パラメータと同じプロトコルに従う限り、make-parameterで作られたのではない 手続きをparamに渡すことができます。 新しいコードでそのような「パラメータっぽい」手続きを動的状態の管理に使いたい場合は、 下のtemporarilyを使ってください (動的状態参照)。
  • このフォーム自体が末尾位置にあっても、bodyの最後の式は 末尾コンテクストでは評価されません。
  • gauche.parameterモジュールで有効になる、 レガシーなパラメータの ’observer’ 機能は、このフォームを使わないと実現できません。 ただ、我々ののこれまでの経験ではこの機能はほとんど使われておらず、 SRFI-226とも非互換なので、今後は非推奨としました。

註:gauche.parameterモジュールをuseすると、組み込みのparameterize の束縛はモジュール独自のparameterizeでシャドウされ、それは parameterize/dynwindの別名となります。これはコードの互換性を保つためです。 新しいコードではgauche.parmaeterをuseすべきではありません。 詳しくはgauche.parameter - パラメータ(レガシー)参照。


6.16.2 動的状態

動的状態とは、特定の動的エクステントに結びつけられた状態です。 (SRFI-226では「パラメータのようなオブジェクト(parameter-like object)」と 呼ばれています)。

動的状態は次のプロトコルを持つ手続きにより実現されます:

parameterizeのかわりにtemporarilyマクロを使って 状態を動的に変えることができます。

(define state
  (let val 0
    (case-lambda
      [() val]
      [(newval) (set! val newval)])))

(define (get-state) (state))

(get-state) ⇒ 0

(temporarily ([state 1])
  (get-state)) ⇒ 1

値を変えるだけなら、これは(converterなしの)パラメータとparameterizeを 使うのと大して変わらないように見えるでしょう。 けれど、parameterizeと違って、temporarilyの動的環境から 出たり再入したりした場合に、手続きが1引数で呼ばれることが保証されています。 従って、その時に外部リソースの状態も一緒に変えるなど、 値を保存する以外のアクションを取ることができます。

Macro: temporarily ((proc value) …) body …

[SRFI-226] まずprocvalueが評価されます。procは動的状態、 すなわち0個または1個の引数をとる手続きを返さなければなりません。

そして、procが保持する値がvalueに置き換えられ、 body … が評価されます。制御がbody …を 離れる際に、procが保持する値が元の値に復元されます。 bodyの最後の式の値がtemporarilyの値となります。

制御がbodyから脱出したり、再入したりする度に、 それぞれのprocが保持する値が回復されます。


6.16.3 パラメータと動的状態の違い

一見すると、パラメータと動的状態はほとんど同じに見えます。 どちらも0個または1個の引数を取る手続きで、 引数無しで呼ばれたら「現在の」値を返し、 引数つきで呼ばれたら「現在の」値を置き換えると。

違いは、パラメータの状態、「パラメタライゼーション」が継続と結びつけられていることです。 parameterizeが評価される継続フレームがポップされると、 それに結びつけられていたパラメタライゼーションは消滅し、 ポップ後に見えている継続に結びつけられていたパラメタライゼーションが 自動的に見えるようになります。つまり、明示的に 「parameterizeの外側の値を回復する」というアクションが不要です。 これが、parameterizeのボディが末尾コンテクストで実行可能な理由です。 ボディを実行した後で値を回復するというアクションを走らせる必要がないからです。

一方、動的状態はdynamic-windのafterサンクを使って状態の復帰を行います。 なのでtemporarilyのボディは末尾コンテクストでは実行されません。

単に値を置き換える以外のアクション、例えば状態の変更をリソース管理モジュールに 通知したい、といったものがあるなら、動的状態を使うのが良いでしょう。 動的状態の値が変更されるときは必ず動的状態手続きが引数つきで呼ばれるので、 その時に必要なアクションをトリガできます。 パラメータだと、動的エクステントを離れる時に値は暗黙に復帰されるので、 値が変わったことの通知を受けることができません。

パラメータはまた、converter手続きを使って、セットされる値を変換することができますが、 動的状態はconvertert手続きを使いません。 というのも、継続によって動的エクステントが一時中断され後で復帰した場合に、 動的状態の値を戻すためにはconverterをバイパスして値をセットしなければならないからです。 そうするためには別のプロトコルが必要ですが、その点に関しては標準と呼べるものがありません。


6.16.4 SRFI-226パラメータへの移行

まず、大まかなルールを:

ボンネットの下:

ここでは0.9.13より前のパラメータ実装を「旧モデル」、 0.9.13とそれ以降を「新モデル」と呼ぶことにします。

旧モデルでは、パラメタライゼーションはdynamic-windのbefore/afterサンクで パラメータの値を入れ替えることで実現されていました。 おおまかに言えば、parameterizeは次のように展開されます (Gaucheのパラメータは変更した時に以前の値を返す、というのを使っています。 また、説明の単純化のためにconverterの処理は省略しています):

(parameterize ((param expr)) body ...)
 ≡
(let ((p param) (v expr))
  (dynamic-wind
   (lambda () (set! v (p v)))
   (lambda () body ...)
   (lambda () (set! v (p v)))))

このモデルはbody …を実行している間、 パラメータの値を直接置き換えています。このモデルを取る場合、 パラメータの値はスレッドごとにせざるを得ません。 でないとあるスレッドが動的スコープに入る度に他のスレッドから見えるパラメータの値が 変わっちゃいます。

このモデルでは、パラメータの値がスレッドごとでも、ある動的環境で捕捉された継続を別のスレッドで 起動したら、捕捉されたパラメータの値は新たなスレッドに引き継がれます。 継続の呼び出し時にdynamic-windのbefore/afterサンクが実行されて、 継続捕捉時のパラメータの値が復元されるからです。

もうひとつ、このモデルでは、parameterizeフォームが末尾位置にあっても bodyは末尾呼び出しできないということに留意してください。 bodyの後でafterサンクを実行しないとならないからです。

新モデルでは、動的束縛のフレームのチェインを持っていて、 パラメータの値はまずそこから探されます。動的束縛のチェインは継続の一部なので、 継続がポップされると、その継続フレームで使われていた動的束縛のフレームも 自動的にポップされます。このモデルではparameterizeは概念的に こんな感じで展開されます:

(parameterize ((param expr)) body ...)
 ≡
((lambda (p v)
   (push-dynamic-frame! p v)
   body ...)
 param expr)

push-dynamic-frame!は新しい動的束縛フレームを 現在の継続にプッシュする仮想的な手続きです。 制御がparameterizeフォームの継続に渡った時には、プッシュされた動的束縛フレームは 自動的にポップされているので、特別な後処理が必要ありません。 body実行中に継続が捕捉された場合、動的束縛フレームのチェインも継続とともに 保存されるので、その継続が別のスレッドで起動されたら、動的束縛フレームも復元されます。 このことから、parameterizeが末尾位置にあれば、 bodyも末尾式として評価されます。

このモデルでは、parameterizeを使わずに直接パラメータの値を変更した場合、 その変更は動的束縛フレームに反映されます。したがって同じ動的束縛フレームが 複数スレッドで共有されていれば、この変更は直ちに他のスレッドからも見えます。

SRFI-226スレッドパラメータは、破壊的変更がスレッド内に止まることを要求します。 なので、スレッドパラメータをparameterizeする度に、スレッドローカルが 作られてパラメータの動的な値はそこに格納されます。 スレッドパラメータの破壊的変更は、同じ動的環境がスレッド間で共有されていたり、 継続が他のスレッドに渡されて実行された場合などでも、他のスレッドには見えません。 スレッドパラメータを使う時はこの点に留意してください。 例えばfutureを作った場合、式は別スレッドで実行されるので、作成側で 見えているスレッドパラメータの値と式内で見えるそれが異なる可能性があります (control.future - Future参照)。 スレッドパラメータはまた、parameterizeごとにスレッドローカルを アロケートするので遅いです。

新モデルでは、parameterizeで動的束縛できるのはパラメータに限られます。 パラメータプロトコルに従っているだけの「パラメータのような」手続きは parameterizeでは処理できません。それらは別にして、 tempporarilyを使ってください (動的状態参照)。

経過措置として、0.9.13のparameterizeは実行時にパラメータが 本当のパラメータであるかどうか検査し、そうでなければ parameterize/dynwindに切り替えて実行します。 ただ、こういった緩い仕様はバグのもとになるので、 将来のリリースではparameterizeはパラメータ以外はエラーにするでしょう。 現在のコードが新モデルで動くかどうかは(use srfi.226.parameter)としておけば 確かめられます。



For Development HEAD DRAFTSearch (procedure/syntax/module):
DRAFT