評価環境のモデル
変数(関数)は,フレームの列からなる環境モデルによって管理されている. フレームとは,変数(もちろんlambda式によって記述される関数も含む)の集まりを格納する入れ物(テーブル)である. またフレームは,それを含む上位の環境(フレームの列)を指し示すポインタを持つ. つまり,あるフレームは別のフレームを指し,さらにそのフレームがまた別のフレームを指すというようにして,フレームの列が形成され,これが環境を定義する. 変数の値は環境を下位のフレームから上位のフレームへと順次調べることで評価される.
この図で太い矢印がフレーム同士の関係を与えている.frame1は最上位のフレームであり上位のフレームを持たない. 上述の通り,環境は(矢印でつながれた)フレームの列で構成される. 例えば環境env4は,frame4,frame2,frame1の3つのフレームの列から構成されている. この環境env4でxの値を評価すると,まず最初のframe4の中で,xに20がバインドされている(対応づけられている)ことが分かるため,評価の結果は20となる. frame2,frame1でもxの値が定められているが,最初に見つけられたframe4での値が有効になる. 環境env4で同様にyの値を評価すると,frame4にはyは見つからないが,上位のフレームframe2でyが15にバインドされているため,評価の結果は15となる. またz,wの値は,frame1での値が有効であり,それぞれ-5,25となる. 一方環境env2においては,y,z,wの値は環境env4で評価したときと同じであるが,xを評価した場合には,frame2での値0が有効になる(環境env2で最初に見つかるxのバインディングが有効).
このように,同じ変数名を使っても,それを評価する環境によって,その評価値を変えることができる. つまり環境モデルによって,同じ変数名を大域変数と局所変数(仮引数やlet式内の変数)で使い分けられることが分かる. またこのモデルが,変数(関数)のスコープを与えることも分かる. 例えばframe1内でバインドされている変数の値は,下位のフレームに同じ変数がない限りいつでも参照できる. つまりframe1内の変数は,スコープに制限のない大域変数である. 逆に,例えばframe6内の変数u,vは,このフレームのみでバインドされており,他からは参照できない局所変数である.
ところで,環境env3でuの値を評価すると,frame3でもframe1でもuは見つからない. このように,ある変数xをある環境で評価するとき,その環境内の全てのフレームを調べて,最上位のフレームでも変数xが見つからなければ,変数xは,(その環境では)バインドされていない変数(unbounded variable)となり,その値を得ることはできない. このときシステムは,エラーを返す.
次の例は,上の図と同じ環境のもとで計算を行ったものである.
> (define x 100) > (define y 30) > (define z -5) > (define w 25) > (+ (let ((x 0) (y 15)) (+ (let ((x 20)) (* 2 x)) (let ((y 30)) y) x y)) (let ((x 10) (z 50)) (* (let ((x 0) (y -10) (z 1) (w 5) (u 200) (v 300)) (+ x y z w u v)) x z)) x y z w) 248235
Schemeに元々定義されている変数(lambda式で記述される関数を含む)を格納しているフレームをグローバルフレームという. グローバルフレームは,最上位のフレームであり上位の環境はない. Scheme処理系を起動した時点では,このグローバルフレームのみからなるグローバル環境が唯一存在する. グローバル環境のもとで,新しく変数(関数)を定義すると,その定義はグローバルフレームに格納される.
> (define x 100) x > (define f (lambda (x) (+ x 1))) f
図のように,関数はその本体(lambda式)を指すポインタと,本体が定義された(与えられた)ときの環境を指し示すポインタを持つ. ここでは,関数によって指されている環境を関数の環境と呼ぶことにする. この場合,fの本体はグローバル環境のもとで与えられているため,fの環境はグローバル環境(グローバルフレーム)である.
関数を呼び出すと,その仮引数の変数に実引数の値をバインドしたフレームが新しくつくられる. このフレームの上位の環境は,その関数の環境である. この新しいフレームを含めたフレームの列からなる環境を用いて,関数が評価される. 例えば,上の図の関数fについて,
> (f 7)を実行したときは,次のような状況になる.
まずグローバル環境で,変数fが調べられる. すると,変数fが関数であることが分かる. ここで,その仮引数xに実引数7をバインドしたフレーム(frame1)が新たに生成される. frame1の上位環境は,関数fの環境(=グローバル環境)である. つまり(f 7)の評価を行う環境env1は,frame1とグローバルフレームで構成され,この環境で関数fの本体であるlambda式
(lambda (x) (+ x 1))が評価される.この環境でxの値は7(frame1での値)であるので,評価値は8になる(※システム関数+の評価を行う過程については省略した).
> (f 7) 8これをみても,関数fの引数xと大域変数xとが独立に扱われていることが分かる. また同じ関数が同時に2回以上呼び出される場合には,それぞれに対して,新しい環境が生成され,それぞれの環境のもとで評価が行われる.
> (* (f 7) (f 9)) 80
これまでにみたように,関数呼出しにおいては,関数本体の定義と呼出しにともなう環境との組が関数の値を評価するために必要な情報を与えるが,この組をクロージャ(closure)という. このクロージャの概念を積極的に用いれば,オブジェクト指向的プログラミングが可能になる.