M.Hiroi's Home Page

お気楽 Standard ML of New Jersey 入門

レキシカルスコープ

Copyright (C) 2005-2020 Makoto Hiroi
All rights reserved.

●レキシカルスコープとダイナミックスコープ

変数の有効範囲を表す用語に「スコープ (scope)」があります。この用語を使うと、厳密ではありませんが、変数の有効範囲は「レキシカルスコープ」と「ダイナミックスコープ」の 2 つに分けることができます。

伝統的な Lisp はダイナミックスコープですが、近代的なプログラミング言語、たとえば Common Lisp, Scheme, SML/NJ はレキシカルスコープを採用しています。

それでは、レキシカルスコープについて詳しく見てみましょう。変数 x を表示する関数 foo を定義します。

- val x = "global\n";
val x = "global\n" : string

- fun foo() = print x;
val foo = fn : unit -> unit

- foo();
global
val it = () : unit

関数 foo には局所変数 x を定義していないので、foo を実行した場合は大域変数の値を参照します。その結果 global が表示されます。それでは、foo1 という関数から foo を呼び出す場合を考えてみましょう。foo1 には let で局所変数 x を定義します。この場合、foo はどちらの値を表示するのでしょうか。実際に試してみましょう。

- fun foo1() = let val x = "local\n" in foo() end;
val foo1 = fn : unit -> unit

- foo1();
global
val it = () : unit

大域変数の値 global を表示しました。このように、foo1 で定義した局所変数 x は、foo から参照することはできません。次の図を見てください。

 ┌────── SML/NJ system  ──────┐ 
 │                                        │
 │              大域変数  x ←────┐  │
 │                                    │  │
 │  ┌→┌─ 関数 foo ──────┐  │  │
 │  │  │          ┌──────┼─┘  │
 │  │  │     print x            │      │
 │  │  │                        │      │
 │  │  └────────────┘      │
 │  │  ┌─ 関数 foo1  ─────┐      │
 │  │  │                        │      │
 │  │  │  ┌─let : x ───┐  │      │
 │  │  │  │                │  │      │
 │  └─┼─┼─ foo()        │  │      │
 │      │  └────────┘  │      │
 │      └────────────┘      │
 │                                        │
 └────────────────────┘

        図 : レキシカルスコープ

上図では変数の有効範囲を枠で表しています。foo1 の let で定義した局所変数 x は、let の枠の中でのみ有効です。もしも、この枠で変数が見つからない場合は、ひとつ外側の枠を調べます。順番に外側の枠を調べていくと、最後には関数定義の枠に行き着きます。ここで変数(引数)が見つからない場合は大域変数を調べます。

関数 foo は関数定義の枠しかありません。そこに変数 x が定義されていないので、大域変数を調べることになるのです。このように、関数 foo から foo1 の枠と let の枠を超えて変数 x にアクセスすることはできないのです。これを「レキシカルスコープ (lexical scope)」といいます。レキシカルには文脈上いう意味があり、変数が定義されている構造の範囲内 (枠内) でないと、その変数にアクセスすることはできません。

ところが伝統的な Lisp の場合、foo1 で定義した変数 x は呼び出された関数 foo からアクセスすることができます。これを「ダイナミックスコープ (dynamic scope)」といいます。foo1 で定義された変数 x は、foo1 の実行が終了するまで存在し、foo1 から呼ばれた関数ならば、どこからでも参照することができます。もしも、foo1 をダイナミックスコープの処理系、たとえば Emacs Lisp で実行するならば、foo で表示される x の値は local になります。

●レキシカルスコープと局所的な関数

それでは、匿名関数や局所的な関数の場合はどうでしょうか。次の例を見てください。

リスト : リストの要素を n 倍する

fun mapcar(_, nil) = nil
|   mapcar(f, x::xs) = f x :: mapcar(f, xs)

fun times_element(n, l) = mapcar(fn x => x * n, l)  

匿名関数の引数は x だけですから、変数 n は大域変数を参照するように思われるかもしれません。ところが、変数 n は関数 times_element の引数 n を参照するのです。これを図に示すと、次のようになります。

 ┌────── SML/NJ system  ─────┐ 
 │                                      │
 │    ┌─ times_element : n, l ─┐    │
 │    │                  ↑      │    │
 │    │                  └─┐  │    │
 │    │  ┌── fn : x ──┐│  │    │
 │    │  │          ↑    ││  │    │
 │    │  │    ┌──┘    ││  │    │
 │    │  │     x * n      ││  │    │
 │    │  │        └───┼┘  │    │
 │    │  └────────┘    │    │
 │    └─────────────┘    │
 │                                      │
 └───────────────────┘

        図 : 匿名関数内の変数

ポイントは、匿名関数が times_element 内で定義されているところです。変数 n は関数の引数として定義されていて、その有効範囲は関数の終わりまでです。匿名関数はその範囲内に定義されているため、変数 n にアクセスすることができるのです。つまり、関数内で定義された匿名関数は、そのとき有効な局所変数にアクセスすることができるのです。

これは let で定義された局所的な関数も同じです。times_element は次のように書き換えることができます。

リスト : リストの要素を n 倍する

fun times_element(n, l) =
  let
    fun timesN x = x * n  
  in
    mapcar(timesN, l)
  end

関数 timesN は times_element 内で定義されているので、timesN から times_element の引数 n を参照することができます。

●クイックソートの改良

簡単な例題として、クイックソートを高階関数に改良してみましょう。データを比較する関数を渡すことで、クイックソートを多相型関数にすることができます。次のリストを見てください。

リスト : クイックソート

fun quicksort(_, nil) = nil
|   quicksort(f, x::xs) =
  let
    (* リストの分割 *)
    fun partition(_, nil, a, b) = (a, b)
    |   partition(z, y::ys, a, b) =
      if f(y, z) then partition(z, ys, y::a, b)  
      else partition(z, ys, a, y::b)

    val (m, n) = partition(x, xs, nil, nil)
  in
    quicksort(f, m) @ (x :: quicksort(f, n))
  end

quicksort の第 1 引数 f がデータを比較する関数、第 2 引数がリストです。リストを分割する関数 partition は quicksort 内で定義しているので、quicksort の引数 f を参照することができます。わざわざ関数 f を partition に渡す必要はありません。なお、partition は末尾再帰でプログラムしています。ご注意ください。

それでは、実行例を示します。

val quicksort = fn : ('a * 'a -> bool) * 'a list -> 'a list

- quicksort(op <, [5,9,1,8,2,7,3,6,4]);
val it = [1,2,3,4,5,6,7,8,9] : int list

- quicksort(op >, [5,9,1,8,2,7,3,6,4]);
val it = [9,8,7,6,5,4,3,2,1] : int list

- quicksort(op <, ["c", "a", "d", "e", "b"]);
val it = ["a","b","c","d","e"] : string list

関数型をみると quicksort は多相型関数として定義されていることがわかります。比較関数を渡すことで、データを昇順でも降順でもソートすることができます。


初版 2005 年 5 月 14 日
改訂 2020 年 8 月 9 日