レキシカル変数

Schemeの変数はレキシカルスコープをもちます。すなわち、変数は プログラムテキストのある特定の連続した広がり内のフォームにだけ 見えています。これまで見てきたグローバル変数も例外ではありません。 グローバル変数のスコープはプログラム全体で、たしかに連続していますね。

すでにいくつかローカル変数の例も見ています。それは、 lambdaパラメータで、この手続きが呼出されるたびに束縛され、 そのスコープはこの手続きの本体です。たとえば、

(define x 9)
(define add2 (lambda (x) (+ x 2)))

x        =>  9

(add2 3) =>  5
(add2 x) =>  11

x        =>  9

ここでは、グローバル変数xとローカル変数xがありますが、 後者は手続きadd2でもたらされたものです。グローバルの xは常に9です。ローカルのxは最初のadd2の呼出しでは 3に束縛されます。2回目の呼び出しではグローバルのxの値、 すなわち、9に束縛されます。この手続きの呼び出しから 返ってくれば、グローバルのxはあいかわらず9です。

フォームset!は変数のレキシカル束縛を変更します。

(set! x 20)

xのグローバル束縛を9から20へ変更します。 それはset!から見えているxの束縛だからです。 もし、set!add2の本体内にあればローカルなxを 変更したでしょう。

(define add2
  (lambda (x)
    (set! x (+ x 2))
    x))

ここのset!はローカル変数xに2を加えます。この手続きはローカルの xの新しい値を返します(効果という意味では、この手続きは前のaddと なんら区別はつきません)。まえと同じようにグローバルのxについて add2を呼んでみましょう。

(add2 x) =>  22

(グローバルのxは今は9ではなく、20であることを思い 出してください。)

add2の内側にあるset!add2が使うローカル変数のxに影響を 与えるだけです。ローカル変数はグローバルxの値に束縛されますが、 後者はローカルのxset!によってひきよせられるような ことはありません。

x =>  20

ここでの議論のすべては、ローカル変数とグローバル変数に 同じ識別子をつかっているからこその議論です。どのような テキストであっても、識別子名xは字面上もっも近い 変数名を参照します。このことは、外側のすべてのグローバルなxを 覆い隠すということです。つまり、add2の中ではパラメータxは グローバルなxを覆い隠しています。

手続きの本体は、それを囲むスコープにある、パラメータが 覆い隠していない変数にアクセスしたり変更したりすることができます。 そうすると次のような興味深いプログラムをつくることができます。

(define counter 0)

(define bump-counter
  (lambda ()
    (set! counter (+ counter 1))
    counter))

手続きbump-counterは引数なしの手続き(サンクとも言う)です。 これは、ローカル変数をもちませんので、なにも覆い隠すものはありません。 呼ばれるたびにグローバル変数counterを変更し、1ずつカウント アップし、counterの現在の値を返します。以下は、連続で bump-counterを呼んだ例です。

(bump-counter) =>  1
(bump-counter) =>  2
(bump-counter) =>  3

5.1  letlet*

ローカル変数は手続きを明示的に生成しなくても導入することができます。 スペシャルフォームletはその本体で利用するローカル変数のリストを 導入します。

(let ((x 1)
      (y 2)
      (z 3))
  (list x y z))
=>  (1 2 3)

lambdaのときと同様、let本体内で、ローカルな x(1に束縛されている)はグローバルなx(これは20に束縛 されています)を覆い隠します。

ローカル変数の初期化(x1に、y2に、 z3に束縛)はletの本体とは考えません。それゆえ、 初期化部でのxへの参照は、グローバルに対する参照であって ローカルのxへの参照ではありません。

(let ((x 1)
      (y x))
  (+ x y))
=>  21

x1に束縛され、yグローバルx すなわち20に束縛されますので、このようになります。

letのレキシカル変数のリストが直列に導入されるほうが 便利なときがあります。つまり、あとからの変数の初期化は 前にでてきた変数のレキシカルスコープ内でおこる ということです。フォームlet*はこれを行います。

(let* ((x 1)
       (y x))
  (+ x y))
=>  2

yの初期化中の、xは直上のxを参照しています。 この例はletが入れ子になった以下のプログラムと完全に同等 (実際以下の場合の便利な省略形として便利)です。

(let ((x 1))
  (let ((y x))
    (+ x y)))
=>  2

レキシカル変数が束縛された値は手続きであってもかまいません。

(let ((cons (lambda (x y) (+ x y))))
  (cons 1 2))
=>  3

このlet本体中では、レキシカル変数consは引数を足します。 外側では、consはあいかわらずドット対を作ります。

5.2  fluid-let

レキシカル変数は、隠蔽されることがなければ、そのスコープの始めから 終りまで見えています。ときには、レキシカル変数を臨時にある値にするのが 便利なことがあります。これを行うにはfluid-letフォームを使います。 1

(fluid-let ((counter 99))
  (display (bump-counter)) (newline)
  (display (bump-counter)) (newline)
  (display (bump-counter)) (newline))

これはletと同じように見えますが、グローバル変数counterを 隠蔽せずに、fluid-let本体へつづける前に、それを臨時に99 に設定します。したがって、本体部のdisplayは以下のような 出力をします。

100 
101 
102 

fluid-let式が評価された後は、グローバルのcounterは、 fluid-letの前の値に復帰します。

counter =>  3

fluid-letletとは全く違う効果があることに注意してください。 fluid-letは、letがやるように、新しいレキシカル変数を導入する、という ことはありません。既存のレキシカル変数の束縛を変更し、fluid-let が済んだとたんにその変更はなかったことになります。

これを理解するために、次のプログラムを考えてみましょう。

(let ((counter 99))
  (display (bump-counter)) (newline)
  (display (bump-counter)) (newline)
  (display (bump-counter)) (newline))

これはひとつ前の例のfluid-letletで置き換えたものです。 出力はこうなります。

4
5
6

すなわち、グローバルのcounterは最初3bump-counter が呼出されるごとに更新されます。新しいレキシカル変数counterは 初期化部で99になっていますがbump-counterの呼び出しにはなんら 影響力をもちません。bump-counterへの呼び出しはローカルのcounter のスコープ内にありますが、bump-counterの本体はそのスコープ内にはない からです。bump-counter本体部はグローバルcounterを参照 しており、その最終の値は6です。

counter =>  6


1 fluid-letは非標準のスペシャルフォームです。Schemeにおける fluid-letの定義については8.3を参照してください。