Schemeのお勉強 その4 レキシカル変数

MIT-Scheme 入れてみた。

 Gauche と他の Scheme 処理系との違いを試す目的で MIT-Schemeをインストールしました。だけど同梱のエディタ edwin は微妙な使い心地だし、コマンドプロンプトから scheme.exe を起動すると専用のシェルウィンドゥが起動しちゃったりとなんか使いにくい……環境への理解不足もあるんだろうけど、Gauche/gosh の使いやすさを実感した気がします。
 他には CommonLisp処理系として、Cygwinclisp, テキストエディタxyzzy。それからMeadowで使ってるEmacsLispを加えれば、5種類ものlisp処理系が入っていることに……ずいぶん入ってるなぁ。処理系がインストールされている言語の中では一番多いかな。

コード表記の変更

 今までは gosh のプロンプト表示をそのまま書いていましたが、行数節約のため変更したいと思います。式の後ろに ;=> を書いてあったらその後ろが式の返す値です。Ruby の #=> のイメージで。
 さて前置きはこの辺にして、本題に。

let式とは

 # let は第一引数にリストをとり、そのリスト内で名前束縛された変数はlet式の中だけで参照できるローカル変数になります。

(define x 100)      ;=> 100
(let ((x 1)) x)     ;=> 1

 let式が返しているのは、グローバル変数 x の値の 100 ではなく、let式内のローカル変数の値。これをlispでは「ローカル変数」では無く「レキシカル変数」「レキシカルスコープ」と呼びます。
 名称はともかく、これはまぁ理解しやすい。他言語でもある話ですよね。C言語だったら。

int x = 100; /* グローバル変数 */
void hoge(void)
{
  int x = 1;    /* 関数内のローカル変数 */
}

 ところが、一つ落とし穴があります。次のように2つのレキシカル変数を定義し、x に 1 を束縛し、y に x を束縛すると……

(let ((x 1) (y x))
  (list x y))       ;=> (1 100)

 x は 1 なんだから、y に x の値を束縛したらこれも 1 になりそうなもの。だけど結果は 100。letの変数定義部で変数に束縛される値は外側のスコープを参照するようです。これは他の言語のローカルスコープ、ローカル変数とは違ってます。もう一度Cで書いてみると。

int x = 100;
void hoge(void)
{
  int x = 1, y = x;
}

 このCのコードなら y は 1 になりますが Scheme の let だと 100 になってしまいます。

let*式

 Scheme(Gauche) にはもう一つレキシカル変数を定義する let* という # があります。 こちらのほうがCのローカル変数宣言に近いようです。

(define x 100)        ;=> 100

(let* ((x 1) (y x))
  (list x y))         ;=> (1 1)

(let* ((y x))
  (list x y))         ;=> (100 100)

 1つ目の let*式では、レキシカル変数 x に束縛した 1 で、y も初期化されています。これは前述した Cの例のように他言語経験者には直感的に理解しやすい結果です。
 2つ目の let*式では、レキシカル変数 x を定義せずに、いきなり y に x を束縛しています。この場合、上位のスコープを参照しグローバル変数 x の値が使用されています。これもCなどと同じ挙動です。

let と let* まとめ

 letとlet*では、レキシカル変数を定義する際「レキシカル変数の値」を「他の変数を参照」して初期化する場合に挙動が異なります。

  • let : 同じレキシカルスコープの変数は参照されない。(上位スコープから検索)
  • let* : 同じレキシカルスコープから参照される。(見つからなければ上位スコープを検索)

 レキシカル変数に束縛する値が全てリテラル(定数)の場合は、let も let* も挙動は同じです。リテラルにはスコープは関係ありませんから。また、明らかに上位スコープにしか参照変数が存在しない場合にも同じ挙動になります。
 ところでグローバル変数が定義されていない状態で同じ実験をしたらどうなるでしょうか? 未定義の変数名 z を使ってやってみ
ます。

(let ((z 10) (x z))
  (list z x))        ;=> ERROR: unbound variable: z

(let* ((z 10) (x z))
  (list z x))        ;=> (10 10)

 let* ではスコープ内で定義した z を参照して x に値が束縛されています。 let ではスコープ内を参照しないのでグローバルスコープで z を探し、存在しないためにエラーになってしまいました。上で書いた結論と矛盾はありませんね。