第10回 データとしての関数2 (6月21日)

今日の課題

■ 代入式の評価

変数の定義は define を使った。

> (define cnt 10)
> cnt
10

代入式の書き方は「(set! 〈変数名〉 〈式〉)」である。

> (set! cnt (+ cnt 1))
> cnt
11

変数の値が書き換えられてしまう(破壊的代入)。こうした機能を許すため、scheme は不純な関数型言語と言われる。

例題1

下記の関数では、関数内にローカルな変数 cnt が使われている。その変数値が書き換えられている。

(define increase
  (lambda ()
    (define cnt 10)
    (write cnt)
    (newline)
    (set! cnt (+ cnt 1))
    (write cnt)
    (newline)))

右記は実行例である。
> (increase)
10
11

(increase) を Interactions 上で何度繰り返しても、表示は「10」と「11」である。繰り返し評価する度にカウントアップする関数を作ることはできないだろうか。そのためには、変数cntと、変数cntを使う関数が消えずに存在し続ける必要がある。

■ クロージャ

クロージャとは、「関数の定義とその計算に必要な変数や関数のまとまり」のことである。

scheme は、ラムダ式を使うと、変数を持つこと(define)と、関数を持つこと(lambda)ができる。具体的には、以下の例題をみてみよう。


例題2

クロージャの例を以下に示す。このクロージャは、変数 cnt とそれを使う関数(ラムダ式(2))を持ち、関数を評価するたびに変数値が増加する。

(define inc
  (lambda () ........................... (1)
    (define cnt 0)
    (lambda () ......................... (2)
      (set! cnt (+ cnt 1))
      cnt)))
> (define a (inc))
> (a)
1
> (a)
2
> (define b (inc))
> (b)
1
> (a)
3
> (b)
2

実行方法は右記の通りである。inc はラムダ式(1) である。(inc) は変数 cnt が 0 となった状態のラムダ式(2) である。したがって、a はラムダ式(2) であり、(a) はラムダ式(2)を評価することを表す。

また、b を新たに定義したが、a の持つ変数 cnt と b の持つ変数 cnt が独立していることがわかる。

■オブジェクト指向への接近

オブジェクト指向プログラミングでは、オブジェクトが、インスタンス変数とメソッドをまとめて持っていた。したがって、オブジェクトはクロージャである。

例題2のクロージャは、変数cntがインスタンス変数に相当している。このクロージャをオブジェクトと呼ぶには不足している点がある。それは、メッセージを受け取り値を返す機能が作られていない点である。そこで、オブジェクトと言えるように、メッセージを受け取り、メッセージの内容に応じて処理を分岐して、適切に値を返すようにしてみよう。

例題3

例題2の inc のラムダ式(2)の部分を次のように改造しよう。引数が「'up」のときは変数cntを1つ増やして値を返し、引数が「'reset」のときは変数cntを0にして値を返す。

(define inc
  (lambda ()
    (define cnt 0) ...................................... インスタンス変数
    (lambda (msg)
      (cond ((eq? msg 'up) (set! cnt (+ cnt 1))  ........ メソッド
                           cnt)
            ((eq? msg 'reset) (set! cnt 0) .............. メソッド
                              cnt)))))
> (define a (inc))
> (a 'up)
1
> (a 'up)
2
> (a 'reset)
0
> (a 'up)
1

実行方法は右記の通りである。

■発展

もっと高級なオブジェクトの実現を目指そう。

「丸い形をしたクッキー」をクロージャとして定義しよう。クロージャとしての説明は次のとおりである:

(define maru-cookie-class
   (lambda () ..................................(1)
     (define radius 0)
     (define size
       (lambda () ..............................(2)
         (* radius radius 3.14)))
     (lambda (msg) .............................(3)
       (cond ((eq? (car msg) 'radius) radius)
             ((eq? (car msg) 'radius:) (set! radius (cadr msg)))
             ((eq? (car msg) 'size) (size))))))

処理の流れから見た説明をしよう。maru-cookie-class という名前にバインドされている内容は、ラムダ式(1)である。ラムダ式(1)は3つの式を順に評価する関数である。

  1. 変数 radius の作成。初期値は 0 である。
  2. 変数 size の作成。ラムダ式(2)は、変数 radius を使い、円の面積を計算する関数である。このラムダ式を変数 size にバインドしている。
  3. ラムダ式の作成。ラムダ式(3)は、引数の msg に応じて処理をする関数である。ここは、ラムダ式(1)の最後に評価されるので、返り値になる。

要点として次のことに注意しよう:

各要点について動作確認をしてみよう:

要点1を確認しよう。maru-cookie-class を評価して、返り値が関数かどうか見てみよう。

> (maru-cookie-class)
#<procedure:7:4>

要点2を確認しよう。maru-cookie-class の返り値である関数を、変数 cc1 としよう。この変数は、中身が関数なので、関数呼出しの式で引数 msg を与えてやると、評価されるはずである。

> (define cc1 (maru-cookie-class))  ← cc1 は #<procedure:7:4>、すなわちラムダ式(3) 
> (cc1 (list 'radius)) ← msg に (list 'radius) がバインドされてラムダ式(3)の呼び出し
0 ← radius の初期値である。
> (cc1 (list 'radius: 5))
> (cc1 (list 'radius))
5 ← 代入がうまくできている。
> (cc1 (list 'size))
78.5 ← 変数 size の関数にも値がうまく伝わっている。
> (cc1 (list 'radius: 10))
> (cc1 (list 'size))
314.0 ← 変数 radius の値を変更することもうまくできている。

要点3を確認しよう。変数 cc2 と cc3 を使って確認しよう。

> (define cc2 (maru-cookie-class))
> (cc2 (list 'radius: 7))
> (define cc3 (maru-cookie-class))
> (cc3 (list 'radius: 9))
> (cc2 (list 'size))
153.86 ← cc2 の radius が 7 のままである。9 になっていないことがわかる。
> (cc3 (list 'size))
254.34 ← cc3 の radius が 9 であることがわかる。

オブジェクトの定義をしてみよう。オブジェクトの定義の大枠は次のとおりである:

(define 〈オブジェクト名〉-class
  (lambda ()
    (define 〈変数名1〉 〈初期値〉)
                  - - -
    (define 〈変数名n〉 〈初期値〉)
    (lambda (msg)
      (cond ((eq? (car msg) 〈メソッド名〉) 〈メソッドの処理〉)
                  - - -
            ((eq? (car msg) 〈メソッド名〉) 〈メソッドの処理〉)))))

練習1

「三角クッキー」と「四角クッキー」のオブジェクトを定義しよう。それぞれ名前は、「sankaku-cookie-class」と「shikaku-cookie-class」とせよ。作成するメソッドは、縦の長さを与えるメソッド height: と参照するメソッド height、横の長さを与えるメソッド width: と参照するメソッド height、および、面積を返す size である。

(実行例)
> (define c2 (sankaku-cookie-class)) ← c2 はsankaku-cookie のインスタンス
> (c2 (list 'width: 10)) ← 幅を10とするメッセージを送信
> (c2 (list 'height: 3)) ← 高さを3とするメッセージを送信
> (c2 (list 'size)) ← 面積計算を要求するメッセージを送信
15.0

練習2

製造日の情報を持つ「マイクッキー(my-cookie-class)」のオブジェクトを定義しよう。インスタンス変数として「pdate(初期値は 0 )」を持つ。「pdate」と「pdate:」により、pdate の値の参照と代入ができるようにせよ。

(実行例)
> (define c3 (my-cookie-class))
> (c3 (list 'pdate: 19))
> (c3 (list 'pdate))
19

■ オブジェクトの継承

擬似変数 self と super

オブジェクトの継承にチャレンジしよう。継承を考えるとき、擬似変数 self と super が重要な役割りを果たすので、まずは、これらの組み込み方を考えてみよう。

  1. self や super は、インスタンスごとに定まるので、これらの変数名は、インスタンス変数と同じ場所で宣言をする。
  2. super は、上位のオブジェクトのインスタンスとする。
  3. self は、インスタンス自身とする。

以上を踏まえ、maru-cookie-class と my-cookie-class を以下のように拡張する。

(define my-cookie-class
  (lambda ()
    (define self ())  ← 追加:代入予定
    (define pdate 0)
    (lambda (msg)
      (cond ((eq? (car msg) 'self:)  (set! self  (cadr msg))) ← 追加:selfに代入
            ((eq? (car msg) 'pdate)  pdate)
	    ((eq? (car msg) 'pdate:) (set! pdate (cadr msg)))))))

(define maru-cookie-class
  (lambda ()
    (define super (my-cookie-class)) ← 追加:上位のインスタンスをsuperに代入
    (define self ())  ← 追加:代入予定
    (define radius 0)
    (define size
      (lambda ()
        (* radius radius 3.14)))
    (lambda (msg)
      (cond ((eq? (car msg) 'self:) (set! self  (cadr msg)) ← 追加:selfに代入
                                    (super msg)) ← 上位のインスタンスにselfが伝播
            ((eq? (car msg) 'radius) radius)
            ((eq? (car msg) 'radius:) (set! radius (cadr msg)))
            ((eq? (car msg) 'size) (size))))))

(define new-instance-of ← インスタンスを作り、擬似変数 self を代入する
  (lambda (class)
    (define sl (class)) ← (〈クラス〉) によりインスタンスを作る。それを sl とする
    (sl (list 'self: sl)) ← 自分自身の擬似変数 self に自分自身を登録。これは上位に伝播
    sl)) ← インスタンスを返す

オブジェクトのインスタンスを得る方法は、たとえば,(my-cookie-class) とする。上記では、この方法を使って、変数 super に上位のオブジェクトのインスタンスを代入した。

ただし、このままでは、疑似変数 self に自分自身が代入されない。疑似変数 self に自分自身が代入されなければならない時は、たとえば、(new-instance-of maru-cookie-class) とする。関数 new-instance-of は、引数に指定されたクラスのインスタンスを生成後、そのインスタンスにインスタンス自身を代入するようにメッセージを送る。下位のオブジェクトは self への代入の際、上位のインスタンスに self が伝播するように処理をする(maru-cookie-class の (super msg) という部分)。

上位オブジェクトのメソッド呼出し

上記のままでは、上位オブジェクトのメソッドを呼び出すことができない。メッセージが自身で処理できない場合、上位で処理をする必要がある。したがって、maru-cookie-class において (cond - - -) の最後に以下を追加する。

(define maru-cookie-class
       - - -
      (cond ((eq? (car msg) 'self:)  (set! self (cadr msg))
                  - - -
            (#t (super msg))) … ← 上位の呼出しのために、追加

それでは動作確認をしよう。

(実行例)
> (define c1 (new-instance-of maru-cookie-class))
> (c1 (list 'pdate: 19))
> (c1 (list 'pdate))
19

練習3

my-cookie-class を改造して、価格を計算するメソッド price を作ろう。面積 1 に対して価格は 3 である。(* (self (list 'size)) 3) という記述を使用せよ。


練習4

maru-cookie-class を参考に、sankaku-cookie-class と shikaku-cookie-class を作ろう。

■ ポリモーフィズム

実は、練習4が完成した時点で、ポリモーフィズムの動作が確認できる。次のとおり実行してみよう。

(実行例)
(define c1 (new-instance-of maru-cookie-class))
(c1 (list 'pdate: 18))
(c1 (list 'radius: 5))
(define c2 (new-instance-of sankaku-cookie-class))
(c2 (list 'pdate: 18))
(c2 (list 'width: 4))
(c2 (list 'height: 3))
(define c3 (new-instance-of sankaku-cookie-class))
(c3 (list 'pdate: 18))
(c3 (list 'width: 4))
(c3 (list 'height: 3))
(define c4 (new-instance-of shikaku-cookie-class))
(c4 (list 'pdate: 20))
(c4 (list 'width: 4))
(c4 (list 'height: 4))

(map (lambda (x) (x (list 'price))) (list c1 c2 c3 c4))

以上のプログラムに少し手を加えたものを、ここに置いている。参照せよ。

練習5

以前の Smalltalk の総合問題を参考に、my-cookie-bag-class を作成しよう。ただし、bag がscheme には実装されていないので、その部分も自作しよう。


(c) 2007.6.18 by tokuhisa