前回と前々回は,関数型言語MLの一種であるObjective Caml(OCaml)で,単純な独自の命令型言語MyCのインタプリタとコンパイラを実装してみた。MyC言語では,変数はすべてグローバルで,宣言も不要だった。
しかし,グローバル変数だけでは名前の衝突などの問題があり,大きなプログラムを書くことは難しい。そのため,ほとんどの汎用プログラミング言語には,変数の有効範囲(スコープ)を制限する機能がある。例えば,すでに何度も登場しているが,OCamlでは次のような形の式を書くことができる。
let x = 式1 in 式2
このように書くと,式1の値が変数xに置かれる(xが束縛される,という言い方をする)。そのxの値は,式2の中でのみ使うことができる。つまり,xのスコープは式2だけということになる(in以降を省略することもできる。その場合はそれ以降のすべての式がスコープに含まれる)。
ただし,もし式2の中でまた同じ名前の変数xが束縛されたら,そちらが優先する。例えば以下のような場合だ。
# let x = 10 in x + (let x = 100 in x + x) + x ;; - : int = 220
上の式には六つのxが出てくるが,まぎらわしいので前から順にx1,x2,x3,x4,x5,x6と書くことにしよう。これはあくまで説明のための表記であって,実際のプログラムに変更はない。
let x1 = 10 in x2 + (let x3 = 100 in x4 + x5) + x6
はじめのlet x1 = 10により,変数x1は整数10に束縛される。x1の有効範囲にあるので,x2の値は10になる。x6も同様だ。しかし,x4とx5は,100に束縛された変数x3の有効範囲にあるので,値が100になる。よって上の式の値は10 + (100 + 100) + 10で220になる。
このように,有効範囲の異なる同名変数が混在していると,人間にとってはわかりにくい。また,インタプリタやコンパイラといった言語処理系にとっても,例えば最適化等の処理がしづらい。そこで,多くの本格的な言語処理系では,あらかじめ変数の名前を付けかえる処理を行う。この処理をα変換という。先のプログラムをα変換すると,例えば次のようになる。
let x = 10 in x + (let x' = 100 in x' + x') + x
これならば,xは10,x'は100と定まっているので,式の値が220になることは明らかだろう。
ネストした定義と静的スコープ
OCamlなどの最近のプログラミング言語では,let構文などを使って変数や関数の定義をネストすることが可能だ。
例として,まず,次のような通常の(ネストしていない)変数と関数の定義を考えてみよう。
# let rate = 118.38 ;; val rate : float = 118.38 # (* 米ドルを日本円に換算 *) let usd_to_yen = fun usd -> usd *. rate ;; val usd_to_yen : float -> float = <fun>
この例では,rateという変数を定義し,それを使ってusd_to_yenという関数を定義している。fun usd -> usd *. rateという部分は,「usdを受け取ってusd *. rateの値を返す」という関数を表す式だ(Lispを知っていれば,lambda構文と同じだと思えばよい)。だから,let usd_to_yen = fun usd -> usd *. rateと書いても,let usd_to_yen usd = usd *. rateと書いても,意味は全く等価になる。
しかし,この定義だと,変数rateのスコープがusd_to_yenの内部だけに制限されておらず,これ以降に入力するすべての式が含まれてしまう。したがって,もしrateという名前の変数がすでに定義されていたら,その値は隠されて見えなくなる。このような名前の衝突を避けるには,次のように書けばよい。
# let usd_to_yen = let rate = 118.38 in fun usd -> usd *. rate ;; val usd_to_yen : float -> float = <fun>
この定義では,let usd_to_yen = ...の右辺に,rateの定義がネストしている。こう書けば,rateのスコープはinの中(fun usd -> usd *. rateの部分)だけに制限されるので,他の定義と衝突する恐れがなくなる。実際にチェックしてみると以下のようになる。
# (* あらかじめrateを定義しておく *) let rate = 3.14 ;; val rate : float = 3.14 # (* usd_to_yenの定義の中で別のrateを定義 *) let usd_to_yen = let rate = 118.38 in fun usd -> usd *. rate ;; val usd_to_yen : float -> float = <fun> # (* トップレベルのrateは3.14のまま *) rate ;; - : float = 3.14 # (* usd_to_yenの中ではrate = 118.38 *) usd_to_yen 100.0 ;; - : float = 11838. # (* rateを定義し直してもusd_to_yenの値は変化しない *) let rate = 123.45 in usd_to_yen 100.0 ;; - : float = 11838.
このように,関数usd_to_yenはusdを受け取ってusd *. rateを計算して返す。rateには,関数が呼び出されるときの束縛ではなく定義されたときの束縛が用いられる。このような動作を静的スコープ(static scoping)という。逆に,関数が呼び出されるときの束縛が用いられるのを動的スコープ(dynamic scoping)という。
上のようなプログラムだけを考えるとややこしいが,例えば,以下のようにα変換したプログラムと見比べれば,静的スコープは自然な動作であることがわかる。要するに,同じ名前の変数rateを繰り返し定義しても,実体はすべて別々の変数(rate, rate', rate'')になっている,ということだ。
# let rate = 3.14 ;; val rate : float = 3.14 # let usd_to_yen = let rate' = 118.38 in fun usd -> usd *. rate' ;; val usd_to_yen : float -> float = <fun> # rate ;; - : float = 3.14 # usd_to_yen 100.0 ;; - : float = 11838. # let rate'' = 123.45 in usd_to_yen 100.0 ;; - : float = 11838.