SICP §2.4(データ主導プログラミングと加法性 その3[データ主導プログラミングと加法性])

前エントリで紹介した手法は、


①汎用インタフェース手続きは全ての実装レベルの手続きを知らなければならず、
②しかもその実装レベルの手続きが増減した場合、その都度汎用インタフェース手続きの修正が必要
というかなり困った手法だ。(汎用インタフェースの実装をを見ればわかるよね。cond の条件を頻繁に修正しなければならないわけで。。)
こういう困った実装方法は、「加法的(additive)ではない」というらしい。つまり困らない実装方法は「加法的である」?最初からそう言えよって感じ?


で、今回の汎用インタフェース手続きが孕んでいる問題を解消するには、「型による振り分け(dispatching on type)」という方法がいいらしい。
イメージとしてはタグ(データのタイプ)と、汎用インタフェースで構成されるテーブルの格子の中に、実装レベルの手続きを入れ込んでいく感じ。
今回の複素数のテーマで考えるとこんなテーブルが構成できる。

PolarRectangular
real-partreal-part-polarreal-part-rectangular
imag-partimag-part-polarimag-part-rectangular
magnitudemagnitude-polarmagnitude-rectangular
angleangle-polarangle-rectangular
このテーブルの実装はとりあえず置いておく。あくまでもイメージね。で、このテーブルを構成するための手続き(実装レベルの手続きを入れたり取り出したりする手続き)はこういうインタフェースを持つとする。
実装レベルの手続きを格納する手続き(put )
実装レベルの手続きを取り出す手続き(get )
さて、上述のの表では格子の内部に実装レベルの手続き名が競合しないように記述されているが、これでは type 情報で整理している意味がない。そこで「パッケージ(package)」という概念を導入する。
パッケージは手続きの内部に更に手続きを定義することにより、外部との名前の競合が発生しないようにすることができる。こんな感じに。

;直交座標形式のパッケージ
(define (install-rectangular-package)
  ;内部手続き
  ;※ install-rectangular-package内で定義されているので、
  ;   次に定義してある install-polar-package 内の同一名称の手続きとは競合が発生しない!
  (define (real-part z) (car z))
  (define (imag-part z) (cdr z))
  (define (make-from-real-imag x y) (cons x y))
  (define (magnitude z)
    (sqrt (+ (square (real-part z))
	     (square (imag-part z)))))
  (define (angle z)
    (atan (imag-part z) (real-part z)))
  (define (make-from-mag-ang r a)
    (cons (* r (cos a)) (* r (sin a))))

  ;システムの他の部分とのインタフェース
  (define (tag x) (attach-tag 'rectangular x))
  (put 'real-part '(rectangular) real-part)
  (put 'imag-part '(rectangular) imag-part)
  (put 'magnitude '(rectangular) magnitude)
  (put 'angle '(rectangular) angle)
  (put 'make-from-real-imag 'rectangular
       (lambda (x y) (tag (make-from-real-imag x y))))
  (put 'make-from-mag-ang 'rectangular
       (lambda (r a) (tag (make-from-mag-ang r a))))
  'done)

;極座標形式のパッケージ
(define (install-polar-package)
  ;内部手続き
  (define (magnitude z) (car z))
  (define (angle z) (cdr z))
  (define (make-from-mag-ang r a) (cons r a))
  (define (real-part z)
    (* (magnitude z) (cos (angle z))))
  (define (imag-part z)
    (* (magnitude z) (sin (angle z))))
  (define (make-from-real-imag x y)
    (cons (sqrt (+ (square x) (square y)))
	  (atan y x)))

  ;システムの他の部分とのインタフェース
  (define (tag x) (attach-tag 'polar x))
  (put 'real-part '(polar) real-part)
  (put 'imag-part '(polar) imag-part)
  (put 'magnitude '(polar) magnitude)
  (put 'angle '(polar) angle)
  (put 'make-from-real-imag 'polar
       (lambda (x y) (tag (make-from-real-imag x y))))
  (put 'make-from-mag-ang 'polar
       (lambda (r a) (tag (make-from-mag-ang r a))))
  'done)

ちなみに put の第2引数のシンボルがリストになっているのが疑問。欄外の注釈にはこんな風に記述されてた。


記号 rectangular ではなく、リスト (rectangular) を使ったのは、同一でない複数個の引数の演算があるかもしれないからだ。
さっぱりわからん。


ま、いーや。じゃ、次に汎用演算を引数に作用させる apply-generic 手続きを紹介しよう。こいつは第1引数に行いたい演算のシンボルを貰い、第2引数以降に作用させたいオブジェクトを可変長で与える。

(define (apply-generic op . args)
  (let* ((type-tags (map type-tag args)) ;第2引数以降のデータオブジェクトからタグだけを抽出
	 (proc (get op type-tags)))      ;タグのシンボルの並びから作用させる手続きを表から抽出
    (if proc
	(apply proc (map contents args))
	(error
	 "No method for these types -- APPLY-GENERIC"
	 (list op type-tags)))))

あ、さっぱりわからんて書いたのってこの汎用演算手続きに関わってきてるのか?次の箇所でどうやって使うかが記述されているけど。。

(define (real-part z) (apply-generic 'real-part z))
(define (imag-part z) (apply-generic 'imag-part z))
(define (magnitude z) (apply-generic 'magnitude z))
(define (angle z) (apply-generic 'angle z))

複数引数の例がねぇじゃねぇかヲイ。次には構成子についての説明しかねぇし。

;直交座標形式の構成子
(define (make-from-real-imag x y)
  ((get 'make-from-real-imag 'rectangular) x y))

;極座標形式の構成子
(define (make-from-mag-ang r a)
  ((get 'make-from-mag-ang 'polar) r a))

こいつは理解できる。


う〜ん、全体的にコンセプトは理解できるんだが、put の第2引数がリストになっていることと、apply-generic 手続きがデータオブジェクトの引数を可変長で持てる意図についてはわからんなぁ。このあとの問題解いていけばわかるんだろうか?


2010/09/09 追記

「さっぱりわからん」の箇所について、見習いschemerさんからコメントをいただきました。ありがとうございます!

まず何がわからんかったのかおさらいすると、


1. put の第2引数のシンボルがリストになっている意味がわからん。
2. だって第2引数て get するときに必要なモノで、シンボル(rectangularとかpolar)毎にパッケージが作られるわけじゃん。
3. だからこのシンボルをリスト化して、'(rectangular XXX) とかしたら、install-rectangular-XXX-package 的なパッケージを追加するんじゃねぇの?ちがうの?
ってとこだった。が、見習いschemerさんからいただいたコメントを元に、どういう意味か少し分かった気がする。
気がするだけかもしれんから、ちょっとまとめてみよう。
見習いschemerさんが提示してくださったのは、equ? という複素数を比較する手続きが題材になっている。

この手続きは引数を二つとり、そのデータ型の並びを元に適切な手続きを get して演算させようという感じ。
具体的に考えると、このような4つの手続きを実装することになる。

比較パターン第1引数第2引数
パターン1①直交座標形式①直交座標形式
パターン2①直交座標形式極座標形式
パターン3極座標形式①直交座標形式
パターン4極座標形式極座標形式
じゃ、実装してみよう。
とりあえず今あるのは直交座標形式と極座標形式だけだからやってみっか!

;直交座標形式×直交座標形式のパッケージ
(define (install-rectangular-rectangular-package)
  ;内部手続き
    
    
    
  (define (equ-r-r? z1 z2)
    (equal? z1 z2)
  
  (define (equ-r-p? z1 z2)
    (equal? z1 (make-from-real-imag (real-part z2)
                                    (imag-part z2))))

  ;システムの他の部分とのインタフェース
    
    
    
  (put 'equ? '(rectangular rectangular) equ-r-r?)
  (put 'equ? '(rectangular polar) equ-r-p?)
  'done)

;極座標形式のパッケージ
(define (install-polar-package)
  ;内部手続き
    
    
    
  (define (equ-p-p? z1 z2)
    (equal? z1 z2))
  
  (define (equ-p-r? z1 z2)
    (equal? z1 (make-from-mag-ang (magnitude z2)
                                  (angle z2)))

  ;システムの他の部分とのインタフェース
    
    
    
  (put 'equ? '(polar-polar) equ-p-p?)
  (put 'equ? '(polar-rectangular) equ-p-r?)
  'done)

こんな感じか。

しかし、ここで各パッケージ内で実装するのは現実的ではないことに気づく。というのは、現時点では複素数の表現方法は直交座標形式と極座標形式の2つしかないからこそ、比較パターンは4つで済んでいるが、これに、③つ目、④つ目、⑤つ目・・・と表現方法が増えていったらどうなるか・・・こんな感じになる。

比較パターン第1引数第2引数
パターン1
パターン2
パターン3
パターン4
パターン5
パターン6
パターン7
パターン8
パターン9
パターン10
パターン11
パターン12
パターン13
パターン14
パターン15
パターン16
パターン17
パターン18
パターン19
パターン20
パターン21
パターン22
パターン23
パターン24
パターン25


その都度全ての組み合わせのパターンで equ? 手続きを定義していかなければならず、これは本来のパッケージの概念にそぐわない。
だっておかしいよね、③つ目の表現が発生したら、①と②のパッケージに修正が入るなんて。③のパッケージだけで独立してて欲しいわけよ。あとでやっぱり③要りませんでした〜ってなったらまた①と②のパッケージに修正が入るんだもの。
となると、equ? に相当するモノは、複素数の構成子のようにパッケージの外に定義されるべきだろう。こんな感じで。

;どのパッケージにも見掛け上は依存しない形で、外部に複素数の比較手続きを定義する。
(define (equ? z1 z2)
  ;ここに記述するのは、異なる表現で受け取る複素数z1、z2を、
  ;うまく比較できるようにするロジック。
  )

上記コード中のコメントにも記述したが、内部のロジックは「複素数を比較する」処理しか記述すべきではない感じ。ではそれをどう実装するべきか。比較する以上、何かしら基準になる同一の表現に変換しなければなるまい。
受け取った引数の、「型タグ」を使って、「自身を標準の表現の複素数に変換して返却する」手続きが必要になる。それは apply-generic を使うことになろう。仮にその変換手続きを convert という名前にすると実装はこんな感じになる。

;自身を標準の表現の複素数に変換して返却する手続き
(define (convert z)
  ;複素数 z の表現に適した変換手続きで変換してもらう感じ。
  (apply-generic 'convert z))

;複素数の比較手続き
(define (equ? z1 z2)
  ;z1、z2 を標準の表現の複素数に変換する「convert」手続きを使用して比較する。
  (equal? (convert z1)
	  (convert z2)))

apply-generic で呼び出される手続きは、「各表現の複素数→標準の表現」に変換する手続きになるから、例えば「標準の表現の複素数を直交座標形式の複素数と約束する」ことにしてやれば、各パッケージに追加する手続きはこんな感じ。

;直交座標形式のパッケージ
(define (install-rectangular-package)
  ;内部手続き
    
    
    
  ;標準型(直交座標形式)へ変換する手続き
  ;(但し、このパッケージの場合は何も変換しない)
  (define (convert z) z)

  ;システムの他の部分とのインタフェース
    
    
    
  ;標準型(直交座標形式)へ変換する手続きを登録
  (put 'convert '(rectangular) convert)
  'done)

;極座標形式のパッケージ
(define (install-polar-package)
  ;内部手続き
    
    
    
  ;標準型(直交座標形式)へ変換する手続き
  (define (convert z)
    (make-from-real-imag (real-part z)
			 (imag-part z)))

  ;システムの他の部分とのインタフェース
    
    
    
  ;標準型(直交座標形式)へ変換する手続きを登録
  (put 'convert '(polar) convert)
  'done)

こうすれば比較ができるようになる。


で、結局このパターンだと、「想定したこと」である複数引数の処理ケースってのは使わないじゃん、というお話。


でもこれって・・・いや勘に過ぎないんだけど、「複素数」というかなり基本的なデータと、「比較する」という処理にフォーカスを当てたからうまく参考ケースとして扱えなかったんじゃないかなと思う。

同じ複素数を扱うにしても、それ以外の引数の表現が一意に決まるようなデータとの演算なら(なんかないかな、科学技術計算的な公式みたいの・・・)うまくいくような気がする。
あ、でもそんな演算は複素数のパッケージに入れるべきじゃないな。アホな依存関係を構築してしまうし。。
やっぱり複数引数のことって考えなくていいんじゃねぇの?