実践Common lispを読み始めた-第16章 オブジェクト指向入門:総称関数(1)

すっかり時間があいてしまった。でも本を読んでなかった訳ではないのでよしとしよう。
今回はオブジェクト指向なので、

C++Javaと比べて何がおいしいの?

というところを中心に読んでいこうと思う。

総称関数とクラス

  • C++/Javaと一緒でlispでもクラスベース。共通の基底クラスTというのがあるというところはJavaと似ている。多重継承ができるところはC++と似ている。
  • 総称関数とかいう抽象的な操作を定義する関数がある。これはC++/Javaにはない。総称関数のよいところは、関数が所属するクラスを明示的に決めなくてよいこと。C++/Javaでは、オブジェクト指向的なメソッドは全てクラスに所属しないといけない。(オブジェクト指向的なというへんな表現を使ったのは、C++だとCの関数も使えるから)例えば、包含関係を持たないクラスのオブジェクトの関連づけを行うメソッドを定義しようと思ったらどっちのクラスにそのメソッドを定義したらいいのか困ることがある。こんなことを考えなくても良さそうだ。具体的に言うと、ManクラスとWomanクラスがあったとして結婚するというメソッドを定義するとするとどちらにつけたらいいのかなみたいなこと。
  • そんでもって、総称関数の実際の処理はメソッドと呼ばれる関数で定義する。上の例で考えると
(defgeneric 結婚する (bride groom))
(defmethod 結婚する ((bride woman) (groom man))
 ...)

と書けるから悩まなくていい。

  • メソッド結合

メソッド呼び出ししたときにどのようにメソッドが呼び出されてるか。
コードを書いてみる。

;;
;; foo.lisp
;;
(defclass A () ())
(defclass B (A) ())
(defclass C (A) ())
(defclass D (B C) ())

(defparameter *a1* (make-instance 'A))
(defparameter *a2* (make-instance 'A))
(defparameter *b1* (make-instance 'B))
(defparameter *b2* (make-instance 'B))
(defparameter *c1* (make-instance 'C))
(defparameter *c2* (make-instance 'C))
(defparameter *d1* (make-instance 'D))
(defparameter *d2* (make-instance 'D))

(defmethod foo ((x A) (y A))
  (format t "A-A~%"))

(defmethod foo ((x A) (y B))
  (format t "A-B~%")
  (call-next-method))

(defmethod foo ((x A) (y C))
  (format t "A-C~%")
  (call-next-method))

(defmethod foo ((x B) (y A))
  (format t "B-A~%")
  (call-next-method))

(defmethod foo ((x B) (y B))
  (format t "B-B~%")
  (call-next-method))

(defmethod foo ((x B) (y C))
  (format t "B-C~%")
  (call-next-method))

(defmethod foo ((x C) (y A))
  (format t "C-A~%")
  (call-next-method))

(defmethod foo ((x C) (y B))
  (format t "C-B~%")
  (call-next-method))

(defmethod foo ((x C) (y C))
  (format t "C-C~%")
  (call-next-method))

(defmethod foo ((x (eql *a1*)) (y (eql *c2*)))
  (format t "a1-c2~%")
  (call-next-method))

(defmethod foo ((x A)(y (eql *c2*)))
  (format t "A-c2~%")
  (call-next-method))

(defmethod foo ((x (eql *a1*)) (y C))
  (format t "a1-C~%")
  (call-next-method))

(defmethod foo((x D) (y D))
  (format t "D-D~%")
  (call-next-method))

(foo *a1* *a2*)
(format t "--~%")
(foo *a1* *b2*)
(format t "--~%")
(foo *a1* *c2*)
(format t "--~%")
(foo *b1* *a2*)
(format t "--~%")
(foo *b1* *b2*)
(format t "--~%")
(foo *b1* *c2*)
(format t "--~%")
(foo *c1* *a2*)
(format t "--~%")
(foo *c1* *b2*)
(format t "--~%")
(foo *c1* *c2*)
(format t "--~%")
(foo *d1* *d2*)

CL-USER> (load "foo.lisp")
; Loading foo.lisp
A-A
--
A-B
A-A
--
a1-c2
a1-c
A-c2
A-C
A-A
--
B-A
A-A
--
B-B
B-A
A-B
A-A
--
B-C
B-A
A-c2
A-C
A-A
--
C-A
A-A
--
C-B
C-A
A-B
A-A
--
C-C
C-A
A-c2
A-C
A-A
--
D-D
B-B ;;(defclass D (B C) ())なので、class Dの次はBクラスを引数にとるメソッドを優先
B-C
B-A
C-B
C-C
C-A
A-B
A-C
A-A
T
CL-USER> 

上のコードでは各メソッドの最後にcall-next-methodを呼んでいるので、最後は(foo (x A)(y A))が呼び出されるんだけど、

  • 引数の型にマッチするメソッドが最初によばれる。
  • call-next-methodによって、引数の型の親クラスにマッチするメソッドが呼ばれる。
  • 引数が複数あったら、前の引数優先でマッチさせる。
  • 多重継承していたら継承しているクラスのリストの先にあるクラス優先でマッチさせる。
  • eqlで引数のインスタンスを特定したメソッドが複数あった場合、より特定されるメソッドが先に呼ばれる。

標準メソッド結合

上のメソッド呼び出しで終わりかと思えば、まだ続く。