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

今日の課題

■ クロージャ

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

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

scheme でも,式の中に複数の式を定義することができ,いずれかの式を選択的に実行することができる.前回の演習では,menseki1 という関数に,"sankaku" という引数を与えると,三角形の面積を計算する関数が得られ,"chouhoukei" という引数を与えると,長方形の面積を計算する関数が得られた.menseki1 を define したものは,definitions の所であったが,この定義は,ある関数の中だけで使えるように定義することができる.

(define menseki-keisan
   (lambda (lst)
     (define menseki
       (lambda (type)
         (cond ((string=? type "sankaku") (lambda (lst) (/ (* (car lst) (cadr lst)) 2)))
               ((string=? type "chouhoukei") (lambda (lst) (* (car lst) (cadr lst)))))))
     ((menseki (car lst)) (cdr lst))))

(lambda (引数の並び) 式の並び) という形式で,lambda は記述する.式 の並びの1つ目が (define menseki - - -) であり,2つ目が ((menseki (car lst)) - - -) である.

上記の実行方法は「(menseki-keisan (list "sankaku" 4 5))」である.

実は,変数の定義を式の並びに記述することができる.

(define hoge
  (lambda (arg)
    (define var1 1)
    (define var2 2)
    (+ var1 var2 arg)))

上記の例は意味の不明な関数 hoge であるが,内部に変数 var1 と var2 を持つことができる.

以上のように,scheme では,関数の定義と変数を持つことができるので,クロージャの要件を満たしている.そして,オブジェクト指向プログラミングに接近することができる.

■ オブジェクトの定義

円オブジェクト

半径を定めることで,円オブジェクトを生成しよう.円オブジェクトは,'radius というメッセージを受け取ると,定義に使った半径のデータを返すという関数を持つことにする.そのためには次のように定義する.

(define circle
  (lambda (r)
    (lambda (msg)
      (cond ((eq? msg 'radius) r)))))

実行例:

(define c1 (circle 2))      ; 半径2の円オブジェクトを生成し,変数c1にバインド
(c1 'radius)                ; 円オブジェクトc1にメッセージ 'radius を送る
=> 2                        ; 返り値として半径が得られた.

練習1

メソッドの追加

練習2

三角形オブジェクト

■ ポリモーフィズム

オブジェクト指向プログラミングの利点として,ポリモーフィズムがあげられる.ポリモーフィズムの性質を備えたプログラミング言語では,データ型の違いを意識して処理を切り換えるようなコードをほとんど書く必要がない.データに対する処理の目的の明確なプログラムを書くことができる.

たとえば,円オブジェクトと三角形オブジェクトの混在しているリストがあるとき,リストの各要素のオブジェクトの面積を計算する様子を考えてみよう.

C言語の場合,次のようになる:

for( i = 0 ; i < size ; i += 1 ){
  if lst[i].type == EN
    printf("%f\n", menseki_en( lst[i].r ));
  elsif lst[i].type == SANKAKU
    printf("%f\n", menseki_sankaku( lst[i].lower, lst[i].height ));
  elsif - - -
}

オブジェクト指向プログラミングの場合,次のようになる:

for( i = 0; i < size ; i += 1 ){
  printf("%f\n", lst[i].menseki);
}

scheme では,次のようになる.

(define each-send
  (lambda (lst msg)
    (if (null? lst)
        ()
        (cons ((car lst) msg) (each-send (cdr lst) msg)))))

(define c1 (circle 2))
(define c2 (circle 3))
(define t1 (triangle 3 4 5))
(define t2 (triangle 1 1 (sqrt 2)))

(each-send (list c1 c2 t1 t2) 'size)

練習3

each-send を collect を使って定義しよう.each-send の動作確認をしよう.

■ インスタンス変数

インスタンス変数は,オブジェクトの個性を表すために必要である.上述の円オブジェクトでは,半径の違いにより,円が区別される.

ここで,変数rは,オブジェクト指向プログラミングでいうところのインスタンス変数に相当する.

純粋な関数型言語では,一度バインドされた変数は,変更されない.しかし,schemeはその点は純粋な関数型言語とは言えず,(set! 変数 式)の形式により変数のバインド値を変更することができる.

(define circle
  (lambda (r)
    (lambda (msg)
      (cond ((eq? 'radius msg) r)
            ((eq? 'twice msg) (set! r (* r 2)))))))

実行例:

(define c1 (circle 10))
(c1 'radius)
=> 10
(c1 'twice)
(c1 'radius)
=> 20

円オブジェクトに名前を付けてみよう.名前は,変数nameに格納することにする.次の定義をする.

(define circle
  (lambda (r)
    (define name "")
    (lambda (msg)
      (cond ((eq? 'radius msg) r)
            ((eq? 'twice msg) (set! r (* r 2)))
	    ((eq? 'name msg) name)
	    ((list? msg)
	     (cond ((eq? 'name (car msg)) (set! name (cadr msg)))))))))

練習4

上記の定義について,自分で実行方法を考えて,動かしてみよう.

解答

今日の練習の答えはここにある.


(c) 2006.6.20 by tokuhisa