Schemeのお勉強 その5 スコープについて(前編)
前回レキシカル変数についてC言語の例をあげて書いてみました。が、どうも腑に落ちない感じがしています。そこで今回はSchemeのスコープについて整理してみます。
変数と関数の名前空間
スコープとはちょっと違いますが、最初に変数・関数の名前空間について。
Schemeでは変数も関数も同じ名前空間に属しています。例えば foo という関数と foo という変数は同じスコープでは共存できません。もし関数foo 導入後に同じ名称の変数foo を定義すると、名前fooは変数に再束縛され、以前の関数を参照できなくなります。
CでもJavaでもPythonでも、同じ識別子は関数・変数関係なく同じスコープに1つしか存在できません。だからSchemeのこの仕様は当たり前のように思えますが、例えばCommonLispでは当たり前ではありません。関数・変数が異なる名前空間に属し defun, defvar のように関数・変数でそれぞれ別扱いする手続きが用意されています。
翻してみれば、Schemeでは関数と変数が同じ扱いだから define 一つですみます。「同じ扱い」というのはSchemeの関数が数値や文字列と区別せずに扱えるオブジェクトであるということで、だから共通の手続きで処理ができ、よりシンプルな実装になるということです。
;; 変数定義 ;; valueという名前を、数値演算式で得られる結果に束縛。 (define value (+ 1 2)) ;; 関数定義 ;; funcという名前をlambda式で作られる関数オブジェクト(クロージャ)に束縛。 (define func (lambda(arg)(処理))
ただし関数定義には利便性のために簡易的な糖衣構文が用意されています。*1
(define (func arg)(処理))
letとdefine
let と define はどちらも名前束縛により変数を導入するスペシャルフォームです。違いはレキシカルスコープを作るか作らないか、導入した変数がレキシカル変数かどうかです。
;; x はグローバルスコープに属する (define x 1) (xを使う処理) ;; y はレキシカルスコープに属する (let ((y 2)) (yを使う処理)
letで導入された変数は let式のスコープ内からしか見えません。しかし define で導入された名前は define式 の外側のスコープに属します。というよりも define式はスコープを作らないと言ったほうがいいのかもしれません。
C/C++では、クラス、関数、if文など { } で作られるブロックはみな個別のスコープになります。一方 Lisp/Scheme では括弧でくくったブロックが必ずしもスコープを作るわけではありません。これまで学んできたフォームからあげるとスコープを作るのは let系とlambda くらいしか思い当たりません。
注意すべき点として、次のような関数定義ではローカルなスコープが作られます。
(define (foo x) (xを使う処理))
引数 x は関数内だけで参照可能なローカル変数です。変数のときにスコープを作らなかった define がここではスコープを作っているように見えますが、この構文がlambdaを省略した糖衣構文だということは既に述べました。次の式と等価です。
(define foo (lambda (x) (xを使う処理)))
スコープを作っているのはあくまでも lambda式で、define によって導入されるのは foo という名前です。fooという関数名自体はdefine 式の外側のスコープに属しています。
関数内関数定義
構造化プログラミングでもオブジェクト指向でも、ローカルでの利用を目的とする変数や関数は外部から見えないようにカプセル化するのが望ましいコーディングスタイルです。必要最低限のインターフェースだけを公開し余計な名前は外部に見せないことで想定外の操作を防ぎます。
C++やJava等ではクラスを作ることで処理のカプセル化を行いますが、SchemeやPythonのように関数内で関数が定義できる言語では、サブルーチンを関数内で定義することでカプセル化を実現できます。
;; 足し算(型チェックあり) (define (add x y) (define (is-valid-type? x y) (and (number? x) (number? y))) (define (do-calc x y) (+ x y)) (define (on-error) (print "Type Error") 0) (if (is-valid-type? x y) (do-calc x y) (on-error)))
このコードはちゃんと機能しますが処理の流れがあちこちに飛び回る読みにくさがあります。関数 add 内の最初にサブルーチンの定義をまとめて行っているからです。C言語等だとこういう処理の流れは珍しくないし贅沢なことかも知れませんが、もっと必要な場所の近くで関数定義できないでしょうか? 例えばこんな風に。
(define (add x y) (if (begin (define (is-valid-type? x y) (and (number? x) (number? y))) (is-valid-type? x y)) (begin (define (do-calc x y) (+ x y)) (do-calc x y)) (begin (define (on-error) (print "Type Error") 0) (on-error))))
多少流れが追いやすくなったと思います。しかしこの関数 add はエラーで定義できません。
ERROR: Compile Error: syntax-error: the form can appear only in the toplevel: (define (is-valid-type? x y) ...
toplevel とありますが、これはグローバルスコープのことではなく関数内の一番最初ということのようです。いずれにしてもこの方法は無理なようです。
lambda を使った方法
lambda式そのものは関数内のどこでも記述できます。無名関数オブジェクトを使って同じ処理を書き直してみます。
(define (add x y) (if ((lambda(x y) ; is-valid-type? (and (number? x) (number? y))) x y) ((lambda(x y) ; do-calc (+ x y)) x y) ((lambda() ; on-error (print "Type Error") 0))))
例題が悪かった……これならサブルーチン使わずに普通に書いたほうがいいですね。ま、それは置いておいて。
lambda式でクロージャを作成すると同時にすぐに引数を渡して呼び出しています。スコープに注目すると、lambda式内のx yは外側のスコープの x yとは関係の無いローカル変数です。C言語風に言うとlambdaの直後の括弧内のx yはローカルスコープに属する「仮引数」で、呼び出しで渡している x y は外側のスコープに属する「実引数」です。
let を使った方法
letはlambdaのようにクロージャを作るわけではありませんが、「レキシカル変数を導入し、ローカルスコープを作る」という類似性から、lambda式を置き換えることが出来ます。
(define (add x y) (if (let ((x x) (y y)) ; is-valid-type? (and (number? x) (number? y))) (let ((x x)(y y)) ; do-calc (+ x y)) (let () ; on-error (print "Type Error") 0)))
注目すべきは、let式はlambdaの時のように一旦クロージャを作って呼び出すというような二段階の手順をとらずに、いきなり式の評価値を返している点。そしてレキシカル変数が関数の引数のように働いている点です。
レキシカル変数導入部 (let ( (x x) (y y) ) ... は一見奇妙に見えますが、letのスコープ仕様を思い出してみると面白いことに気づきます。二つ並んだ x のうち左側はレキシカルスコープ内に導入されたレキシカル変数です。右側の x はレキシカル変数の初期値です。この場合初期値は変数 x の値ということになります。この右側の x は let の仕様からレキシカルスコープ内の変数ではありません。上位のスコープに属する変数になります。
ちょうど 左側の x が「仮引数」、右側の x が「実引数」となっているわけです。let式 はあくまでもただのS式ですが、無名関数呼び出しに似た性質も持ち合わせていることになります。そういう風になるように let は作られているんですね。
後編へつづく。
*1:MIT記法と呼ぶようです。