2015-05-19-01-コンパイラマクロとCL-OVERLOAD - project-enigma

2015-05-19-01-コンパイラマクロとCL-OVERLOAD

>> Site top >> weblog >> 月別アーカイブ >> 2015年05月のlog >> 2015-05-19-01-コンパイラマクロとCL-OVERLOAD

最終更新日付:2015/05/19 22:03:34


コンパイラマクロとCL-OVERLOAD

2015 年 05 月 19 日

ふと気がついた。自分で作って使っている CL-OVERLOAD というライブラリ、コンパイラマクロで実現する方がキレイじゃないだろうか? 今回はそんな話。

先に書いておくべきことを。「C++の関数オーバーロードみたいなことを Common Lisp でやりたいのだが、マクロで実現するしかないっぽい」というようなことを Twitter で書いていたら、「コンパイラマクロを使うか、MOPでどうにかするしかない」という指摘を頂いたことがあったのだ。その頃はコンパイラマクロを完全に理解していなかったし、コンパイラマクロ「だけで」なんとかするということに気をとられてしまったため、その時は「コンパイラマクロではできない」と結論してしまったのだ。だがそれは間違いだった。マクロとコンパイラマクロを組み合わせれば良かったのだ。ようやく当時頂いた指摘に追い付けたような格好だ。当時御指摘下さった方々に感謝します。

さて、このCL-OVERLOAD というのは、引数の数で関数をオーバーロードする仕組みだ。つまり C++ の関数オーバーロードを部分的に模倣しようというモノだった。引数リストが同一であれば、Common Lisp は総称関数の仕組みを使ってメソッドを呼び分けることができる。これにかぶせるラッパーとして作られたのが CL-OVERLOAD というわけだ。

細かい話は省くとして、とりあえず関数オーバーロード foo を宣言しよう。とりあえず、引数は「1つの場合」と「2 または 3 の場合」でオーバーロードする。foo に対する setf も合わせて宣言すると、以下のようになる。

(ol:declare-overload foo (1 2-3) :make-setf t)

 

無論、declare-overload はマクロだ。肝心なのは、これがどう展開されるかだ。読み易さのために整形(と gensym の改名)をしているが、おおむね以下のように展開される。

(progn
 (defgeneric __foo-1   (v1))
 (defgeneric __foo-2-3 (v1 v2 &optional v3))
 (defgeneric (setf __foo-1)   (newval v1))
 (defgeneric (setf __foo-2-3) (newval v1 v2 &optional v3))
 (defmacro __foo-method& (setf-p cnt)
   (if setf-p
       (cond ((= 1 cnt)    #'(setf __foo-1))
             ((<= 2 cnt 3) #'(setf __foo-2-3)))
       (cond ((= 1 cnt)    #'__foo-1)
             ((<= 2 cnt 3) #'__foo-2-3))))
 (defmacro foo (&rest args)
   nil
   (let ((cnt (length args)))
     (cond ((= 1 cnt)    `(,'__foo-1 ,@args))
           ((<= 2 cnt 3) `(,'__foo-2-3 ,@args))
           (t (error "can't resolve overload for ~a" 'foo))))))

 

これはまだ現状の CL-OVERLOAD なので、foo はマクロとして定義される。そのせいで#' オペレータで関数そのものを取得できないため、CL-OVERLOAD には method& というオペレータが用意されている。上記展開結果中の __foo-method& はそのためのヘルパー関数だ。つまり、CL-OVERLOAD がコンパイラマクロで刷新された暁には要らなくなるものということになる。実は、この method& を抹殺したいというのがコンパイラマクロ化の隠れた目的だったりする。

では、仮にコンパイラマクロで刷新した場合、上記の展開結果はどのように変わるべきだろう? __foo-method& は要らなくなり、foo マクロはコンパイラマクロになる。そして、同じ foo という名前で関数が追加されるだろう。では、やってみよう。

‥‥‥やってみた結果、関数とコンパイラマクロは foo と (setf foo) の両方で定義してやらなければならないことがわかった。結果、以下のようになった。

(progn
 (defgeneric __foo-1   (v1))
 (defgeneric __foo-2-3 (v1 v2 &optional v3))
 (defgeneric (setf __foo-1)   (newval v1))
 (defgeneric (setf __foo-2-3) (newval v1 v2 &optional v3))
 (defun foo (&rest args)
   (format t "called normal version of foo with ~A.~%" args)
   (let ((cnt (length args)))
     (cond ((= 1 cnt)    (apply #'__foo-1 args))
           ((<= 2 cnt 3) (apply #'__foo-2-3 args))
           (t (error "can't resolve overload for ~a" 'foo)))))
 (define-compiler-macro foo (&rest args)
   (let ((cnt (length args)))
     (cond ((= 1 cnt)    `(,'__foo-1 ,@args))
           ((<= 2 cnt 3) `(,'__foo-2-3 ,@args))
           (t (error "can't resolve overload for ~a" 'foo)))))
 (defun (setf foo) (newval &rest args)
   (format t "called normal version of (setf foo) with ~A.~%" args)
   (let ((cnt (length args)))
     (cond ((= 1 cnt)    (apply #'(setf __foo-1)   newval args))
           ((<= 2 cnt 3) (apply #'(setf __foo-2-3) newval args))
           (t (error "can't resolve overload for ~a" 'foo)))))
 (define-compiler-macro (setf foo) (newval &rest args)
   (let ((cnt (length args)))
     (cond ((= 1 cnt)    `(setf (,'__foo-1 ,@args) ,newval))
           ((<= 2 cnt 3) `(setf (,'__foo-2-3 ,@args) ,newval))
           (t (error "can't resolve overload for ~a" 'foo))))))

 

結果的には、マクロ展開結果のボリュームは増大することになる。では引き換えになるメリットはなんだろう? 言うまでもなく、#' オペレータで関数を取得できることだ。メリットは他にはないだろうか? ひとまず思いつかない。

では、デメリットの方は? 他にないだろうか? ちょっと気になるのは、コンパイラマクロが効かない条件だ。たとえば、#'foo で関数を取って、結果に対して funcall や apply をした場合、コンパイラマクロでなく関数版の foo がコールされる。それは引数の数によるディスパッチを実行時に行なうことを意味するわけだ。一方、既存実装で提供している method& はダミーの引数パターンを与えることでコンパイル時点で総称関数を決定するから、この点については「後退」していることになる。括弧付きで書いたのは、気にするほどのことじゃないかもしれないからだ。実際そうなのだろう。

以上、ひとまず関数オーバーロードについてはコンパイラマクロ化していいようだ。だが、CL-OVERLOAD にはあと2つ、以下のオーバーロード機構があるのだ。

マクロについては、まぁ気にすることはないと思っている。なんといってもマクロなのだから。問題はコンストラクタだ。これもおそらくコンパイラマクロに落とし込めるだろうが、ここから先は次回以降の話。

 

コメント

このページにコメントする

 

このページのタグ

Page tag : Common Lisp

 

 


Copyright(C) 2005-2017 project-enigma.
Generated by CL-PREFAB.