マクロを使ったジェネリック関数宣言の一例
x, y 座標をスロットに持つ vec クラスを考える。Common Lisp では次のような定義になる。
(defclass vec () ((x :initarg :x :initform 0d0 :accessor vec-x) (y :initarg :y :initform 0d0 :accessor vec-y)))
ついでに factory も定義しておく。
(defun make-vec (x y) (make-instance 'vec :x x :y y))
この vec のインスタンス同士で四則演算を行いたいので次のような関数を考える。
(defun v+ (v1 v2) (make-vec (+ (vec-x v1) (vec-x v2)) (+ (vec-y v1) (vec-y v2))))
もちろん v1, v2 を vec のインスタンスに限らずに、スカラー値も渡したいのでジェネリック関数として定義しておいて、オブジェクトによって使う関数を変えることにする。
(defgeneric v+ (a b) (:documentation "...")) (defmethod v+ ((a vec) (b vec)) (make-vec (+ (vec-x a) (vec-x b)) (+ (vec-y a) (vec-y b)))) (defmethod v+ ((a vec) b) (make-vec (+ (vec-x a) b) (+ (vec-y a) b))) (defmethod v+ (a (b vec)) (make-vec (+ a (vec-x b)) (+ a (vec-y b))))
これで
(v+ (make-vec 1d0 2d0) (make-vec 0d0 0d0)) ;; => (1d0, 2d0) (v+ 1d0 (make-vec 1d0 1d0)) ;; => (2d0, 2d0) (v+ (make-vec 1d0 0d0) 3d0) ;; => (4d0, 3d0)
のように汎用的に使えるようになった。
しかし、一連の defmethod の定義をそれぞれの演算子に対して行うのは面倒だ。パターンを見つけたらマクロを使う。defvec-op マクロは関数名と関数を引数として、上で定義したような複数のジェネリック関数に展開してくれるマクロとする。途中は省略して、最終的なマクロは次のようになった。
(defmacro defvec-op (method proc &optional (doc nil)) `(progn (defgeneric ,method (a b) (:documentation ,doc)) (defmethod ,method ((a vec) (b vec)) (make-vec (funcall ,proc (vec-x a) (vec-x b)) (funcall ,proc (vec-y a) (vec-y b)))) (defmethod ,method ((a vec) b) (make-vec (funcall ,proc (vec-x a) b) (funcall ,proc (vec-y a) b))) (defmethod ,method (a (b vec)) (make-vec (funcall ,proc a (vec-x b)) (funcall ,proc a (vec-y b))))))
このマクロを使って、四則演算を定義しよう。
(defvec-op v+ #'+) (defvec-op v- #'-) (defvec-op v* #'*) (defvec-op v/ #'/)
めでたしめでたし。
さて、同じようなことを Scheme でもやってみたい。Scheme には標準仕様としてのオブジェクト・システムが存在しないのでここでは Gauche を代表例にする。Scheme のマクロはまだよくわからない事も多く、果たして正しい書き方か不安だが次のように書いてみた。
(define-class <vec> () ((x :init-value 0.0 :init-keyword :x :accessor vec-x) (y :init-value 0.0 :init-keyword :y :accessor vec-y))) (define (make-vec x y) (make <vec> :x x :y y)) (define-syntax define-vec-op (syntax-rules () ((_ method proc) (begin (define-generic method) (define-method method ((a <vec>) (b <vec>)) (make-vec (proc (vec-x a) (vec-x b)) (proc (vec-y a) (vec-y b)))) (define-method method ((a <vec>) b) (make-vec (proc (vec-x a) b) (proc (vec-y a) b))) (define-method method (a (b <vec>)) (make-vec (proc a (vec-x b)) (proc a (vec-y b)))))))) (define-vec-op v+ +) (define-vec-op v- -) (define-vec-op v* *) (define-vec-op v/ /)
Scheme の define-syntax は Common Lisp のマクロと違って、変数の面倒を見てくれるので楽だし、見やすい。さきほどの Common Lisp 版も一続きのコードとして再掲する。
(defclass vec () ((x :initarg :x :initform 0d0 :accessor vec-x) (y :initarg :y :initform 0d0 :accessor vec-y))) (defun make-vec (x y) (make-instance 'vec :x x :y y)) (defmacro defvec-op (method proc &optional (doc nil)) `(progn (defgeneric ,method (a b) (:documentation ,doc)) (defmethod ,method ((a vec) (b vec)) (make-vec (funcall ,proc (vec-x a) (vec-x b)) (funcall ,proc (vec-y a) (vec-y b)))) (defmethod ,method ((a vec) b) (make-vec (funcall ,proc (vec-x a) b) (funcall ,proc (vec-y a) b))) (defmethod ,method (a (b vec)) (make-vec (funcall ,proc a (vec-x b)) (funcall ,proc a (vec-y b)))))) (defvec-op v+ #'+) (defvec-op v- #'-) (defvec-op v* #'*) (defvec-op v/ #'/)
やはり、Scheme の方が全体として美しい。Scheme の関数と変数の名前空間を一緒に扱う設計によってより簡潔になっている。