define を使うと変数を作ることができる。
一方、set! を使うと変数の書き換えができる。これを破壊的代入という。
(define a 0) (set! a (+ a 1)) |
対話エリアで動作確認をする場合、set! のかわりに、(define a (+ a 1)) を使っても動作する。しかし、ラムダ式の中では、set! でないと動作しない。理由は、変数名を管理する都合のためである。
ラムダ式を使用すると、変数名が管理されるようになる。クロージャとは簡単に言えば、管理表である。ラムダ式を評価して #<procedure> が作られる度に、管理表(=クロージャ)が作られる。
ラムダ式を評価して、管理表が存在していることを体感してみよう。
; (定義エリアで記入しよう) (define counter (lambda (x) (define n x) (define up (lambda (d) (set! n (+ n d)) n)) up)) |
上記の関数 counter は、変数 n を使用する。その状況で、n のカウントを上げる関数 up が定義されている。counter は、関数 up を返す高階関数である。
ゆえに、次のとおりに実行すると返り値は関数 up である。
; (対話エリアで試してみよう) > (counter 0) #<procedure:up> |
では、この返り値を変数 c1 で捕まえておくには、次のとおりに実行する。
> (define c1 (counter 0)) |
ここで、c1 の正体は関数 up なので、引数を 1 つとり、上記のプログラムでは変数 d がその引数である。ゆえに、次のように実行することができる。
> (c1 2) 2 > (c1 2) 4 > (c1 2) 6 |
関数 up には変数 n を使う管理表(クロージャ)がくっついている。消されずに残っている。ゆえに、(c1 2) を評価するごとに変数 n が増加してく。
ひきつづき、以下を実行してみる。
> (define c2 (counter 0)) > (c2 2) 2 > (c2 2) 4 |
c2 の正体は関数 up である。同様に値が増加していくことがわかる。
さらに、c1 を再度評価してみる。
> (c1 2) 8 |
c2 の実行状況によらず c1 は以前の状態からカウントが増えている。変数 n について、c1 の n と c2 の n とは区別されて、管理されていたことが分かる。
ここで、オブジェクト指向プログラミングを思い出してみよう。オブジェクトには次の3つの特徴があった。
上記の counter は、メッセージの受信の処理ができていないので、拡張をしてみよう。具体的に以下のとおりに実行できるようにするにはどうすれば良いだろうか?
> (define c3 (counter 0)) > ((c3 'up) 2) 2 > ((c3 'up) 2) 4 |
メッセージ 'up のときは関数 up を返すので、次のようにすればよい。
(define counter (lambda (x) (define n x) (define up (lambda (d) (set! n (+ n d)) n)) (define receive-message (lambda (msg) (cond ((equal? msg 'up) up)))) receive-message)) |
カウンタにリセット機能 'reset を追加しよう。以下の動作を確認しよう。
> (define c4 (counter 0)) > ((c4 'up) 2) 2 > ((c4 'up) 2) 4 > ((c4 'reset)) 0 > ((c4 'up) 2) 2 |
順序不問で値を格納するオブジェクトを作成しよう。このオブジェクトは、次の3つのメッセージを受理することとする。また、未知のメッセージの場合 (write 'error) を実行せよ。
以下に実行例を示す。
> (define b1 (bag)) > ((b1 'show)) () 空のリスト > ((b1 'add) 'abc) > ((b1 'add) 'def) > ((b1 'add) 'ghi) > ((b1 'show)) (ghi def abc) 順はこのとおりでなくても良い > ((b1 'get-one)) ghi 何か要素が返される。 > ((b1 'show)) (def abc) 要素が減らされている。 > ((b1 'clear)) error 未知のメッセージなので error と表示する。 > (define b2 (bag)) > ((b2 'get-one)) error 空のときは error と表示する。 |