「パラメータ」は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パラメータへの移行参照)。
• パラメータ: | ||
• 動的状態: | ||
• パラメータと動的状態の違い: | ||
• SRFI-226パラメータへの移行: |
歴史的事情から、パラメータには、少しだけ振る舞いが違う3つの種類があります。 但し、その違いはマルチスレッドコードで破壊的変更をしない限りは見えません。 R7RSの範囲、すなわちパラメータを動的束縛にだけ使っている場合は、 どのパラメータも同じように振る舞います。
その3つの種類は、共有パラメータ、スレッドパラメータ、
そしてレガシーパラメータです。
最初のふたつはSRFI-226で定義されます。
(SRFI-226では「共有パラメータ」とは呼んでいませんが、make-parmaeter
で
作られる通常のパラメータが共有パラメータになります)。
最後のものはGauche 0.9.12以前と互換なパラメータです。
次の表に、パラメータの破壊的変更が他のスレッドから見えるかどうかをまとめます。
共有 | スレッド | レガシー | |
---|---|---|---|
トップレベルの変更が見えるか? | yes | no | no |
動的範囲での変更が見えるか? | yes | no | yes |
ここで「トップレベルの変更」とはparameterize
がされていない動的環境での
変更、「動的範囲での変更」とはparameterize
が有効な動的環境での変更です。
(define p (make-parameter 0)) (p 1) ; トップレベルの変更 (parameteize ((p 2)) (p 3) ; 動的範囲での変更 ) (p 4) ; トップレベルの変更
共有パラメータとレガシーパラメータについては、 動的範囲での変更が見えるのは、同じ動的環境を共有しているスレッド間のみです。
[R7RS base][SRFI-226] 初期値がvalueであるパラメータを作成します。 もし省略可能な引数converterが与えられた場合、 それは一つの引数を取る手続きでなければなりません。 パラメータの値が変更されようとした時、converterは与えられた値を 引数として呼ばれ、converterが返した値がパラメータの新しい値と なります。converterはエラーを報告したりパラメータの値を変えずに置くことも 可能です。
上で説明したように(パラメータ参照)、パラメータには3種類あります。
make-shared-parameter
、make-thread-parameter
、
make-legacy-parameter
はそれぞれ
共有パラメータ、スレッドパラメータ、レガシーパラメータを作ります。
互換性のため、make-parameter
はどのモジュールをuseするかで
動作が変わります。
gauche
モジュールのmake-parameter
は、0.9.13では
互換性のためにmake-legacy-parameter
と同じ動作ですが、
将来はmake-shared-parameter
に切り替わります。
以前のコードを使いつづけるには、gauche.parameter
モジュールをuseするか、
make-parameter
をmake-legacy-parameter
に置き換えてください。
gauche.parameter
モジュールをuseすると、
make-parameter
はmake-legacy-parameter
と同じ動作になります。
これは今後も変わりません。gauche.parameter
は互換性のためのモジュールとなります。
srfi.226
をuseした場合、make-parameter
はSRFI-226で
定義されているとおり、すなわちmake-shared-parameter
の動作となります。
註: R7RSはmake-parameter
を、
SRFI-226はmake-parameter
とmake-thread-parameter
を
定義しています。
他のふたつ、make-shared-parameter
と
make-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?
を使うように変更する必要があります。
[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
[SRFI-226]
objがmake-parameter
で作られたものであった場合は#t
を、
そうでなければ#f
を返します。
[SRFI-226]
現在のパラメータの動的束縛の状態を具体化したパラメタライゼーションオブジェクト
を返します。パラメタライゼーションはcall-with-parameterization
に
渡すことで別の継続へと受け渡すことができます。
[SRFI-226]
objがパラメタライゼーションなら#t
を、そうでなければ#f
を返します。
[SRFI-226]
call-with-parameterization
の呼び出しの継続から
パラメタライゼーションだけparameterizationと置き換えた継続のもとで
thunkを呼び出します。
すなわち、この手続きが末尾コンテクストで呼ばれたなら、
thunkの呼び出しも末尾コンテクストになります。
これはdynamic-wind
を使ってパラメータ束縛を管理するフォームです。
0.9.13より前のparameterize
と全く同じ動作をします。
このフォームは主に、レガシーコードを動かすために提供されています。
これは現在のparameterizeと、次の点が異なります。
make-parameter
で作られたのではない
手続きをparamに渡すことができます。
新しいコードでそのような「パラメータっぽい」手続きを動的状態の管理に使いたい場合は、
下のtemporarily
を使ってください (動的状態参照)。
gauche.parameter
モジュールで有効になる、
レガシーなパラメータの ’observer’ 機能は、このフォームを使わないと実現できません。
ただ、我々ののこれまでの経験ではこの機能はほとんど使われておらず、
SRFI-226とも非互換なので、今後は非推奨としました。
註:gauche.parameter
モジュールをuseすると、組み込みのparameterize
の束縛はモジュール独自のparameterize
でシャドウされ、それは
parameterize/dynwind
の別名となります。これはコードの互換性を保つためです。
新しいコードではgauche.parmaeter
をuseすべきではありません。
詳しくはgauche.parameter
- パラメータ(レガシー)参照。
動的状態とは、特定の動的エクステントに結びつけられた状態です。 (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引数で呼ばれることが保証されています。
従って、その時に外部リソースの状態も一緒に変えるなど、
値を保存する以外のアクションを取ることができます。
[SRFI-226] まずprocとvalueが評価されます。procは動的状態、 すなわち0個または1個の引数をとる手続きを返さなければなりません。
そして、procが保持する値がvalueに置き換えられ、
body … が評価されます。制御がbody …を
離れる際に、procが保持する値が元の値に復元されます。
bodyの最後の式の値がtemporarily
の値となります。
制御がbodyから脱出したり、再入したりする度に、 それぞれのprocが保持する値が回復されます。
一見すると、パラメータと動的状態はほとんど同じに見えます。 どちらも0個または1個の引数を取る手続きで、 引数無しで呼ばれたら「現在の」値を返し、 引数つきで呼ばれたら「現在の」値を置き換えると。
違いは、パラメータの状態、「パラメタライゼーション」が継続と結びつけられていることです。
parameterize
が評価される継続フレームがポップされると、
それに結びつけられていたパラメタライゼーションは消滅し、
ポップ後に見えている継続に結びつけられていたパラメタライゼーションが
自動的に見えるようになります。つまり、明示的に
「parameterize
の外側の値を回復する」というアクションが不要です。
これが、parameterize
のボディが末尾コンテクストで実行可能な理由です。
ボディを実行した後で値を回復するというアクションを走らせる必要がないからです。
一方、動的状態はdynamic-wind
のafterサンクを使って状態の復帰を行います。
なのでtemporarily
のボディは末尾コンテクストでは実行されません。
単に値を置き換える以外のアクション、例えば状態の変更をリソース管理モジュールに 通知したい、といったものがあるなら、動的状態を使うのが良いでしょう。 動的状態の値が変更されるときは必ず動的状態手続きが引数つきで呼ばれるので、 その時に必要なアクションをトリガできます。 パラメータだと、動的エクステントを離れる時に値は暗黙に復帰されるので、 値が変わったことの通知を受けることができません。
パラメータはまた、converter手続きを使って、セットされる値を変換することができますが、 動的状態はconvertert手続きを使いません。 というのも、継続によって動的エクステントが一時中断され後で復帰した場合に、 動的状態の値を戻すためにはconverterをバイパスして値をセットしなければならないからです。 そうするためには別のプロトコルが必要ですが、その点に関しては標準と呼べるものがありません。
parameterize
で束縛しているだけなら、違いはありません。
gauche.parameter
をuseしている場合は、
以前の振る舞いのままになります。レガシーコードが壊れてしまうことはありません。
parameterize
で変更している場合は、
かわりにtemporarily
を使うようにします
(動的状態参照)。
make-parameter
とparameterize
のかわりに
make-thread-parameter
とparameterize/dynwind
を使うようにします。
ここでは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)
としておけば
確かめられます。