八発白中

技術ブログ、改め雑記

なぜ僕はcl-annotを使うのか

Twitterで、なぜcl-annotを使うのかという主旨の質問を受けて、そういえば id:m2ymModern Common Lispに書くだろうからと言ってほとんど利点をまとめてなかった気がします。140字で返信するのは不可能なのでついでにブログに書いておきます。

cl-annotとは

Common Lispアノテーションを使うためのリードマクロライブラリです。

代表的なものに @export があります。

@export
(defvar *default-fizzbuzz-count* 20)

@export
(defun fizzbuzz (&optional (n *default-fizzbuzz-count*))
  (loop for i from 1 to n
        if (zerop (mod i 15))
          collect 'fizzbuzz
        else if (zerop (mod i 3))
          collect 'fizz
        else if (zerop (mod i 5))
          collect 'buzz
        else
          collect i))

使い方は見ての通りで、通常の変数や関数宣言の前に @export とつけるだけです。cl-annotはその他にdefmacroやdefstruct、defclass、defmethodなども当然のように対応しています。

cl-annotはClackCavemanを始め、CL-DBI、CL-Projectなど、僕が作ったライブラリのほとんどで使われています。

なぜcl-annotを使うのかを急に話してもきっと意味がわからないので、cl-annotの特徴から順を追って説明したいと思います。

一般性と透過性

まず重要なのは、その透過性です。リードマクロと言うと、特別なシンタックスを持ち出していると思われがちですが、cl-annotはもっと一般的で単純な変換ルールを持っています。

たとえば @export は以下のように展開されます。

@export
(defvar *default-fizzbuzz-count* 20)

;; ↑は↓のようになる

(progn
  (export '*default-fizzbuzz-count*)
  (defvar *default-fizzbuzz-count* 20))

また、@export はcl-annotが提供しているアノテーションですが、実は一般的なあらゆる関数、マクロ、スペシャルフォームもアノテーションとして書くことができます。

@print 'nitro_idiot
;-> NITRO_IDIOT
;=> NITRO_IDIOT

@quote hatena
;=> HATENA

これは、「@」のあとに書かれたものが特別に定義されたアノテーションでない場合、引数1つを取るS式として変換するためです。

@print 'nitro_idiot
;<=> (print 'nitro_idiot)

@quote hatena
;<=> (quote hatena)

実はもともと @export アノテーションも同様の扱いをしていました。関数の場合は以下のように変換されたとしても動作します。

(export
  (defun fizzbuzz (&optional (n *default-fizzbuzz-count*))
    (loop for i from 1 to n
          if (zerop (mod i 15))
            collect 'fizzbuzz
          else if (zerop (mod i 3))
            collect 'fizz
          else if (zerop (mod i 5))
            collect 'buzz
          else
            collect i)))

ただ、defclassがsymbolではなくクラス自身を返すため、一度マクロを挟んでいるわけです。

拡張性がある

@export はcl-annotが提供する、標準アノテーションですが、自分が欲しいアノテーションがあれば自由に定義することもできます。

たとえば、Cavemanでは @url という独自のアノテーションを定義しています。

@url GET "/:member/:id"
(defun member-profile (params)
  (render "profile.html" params))

これは以下のように定義されています。

(defannotation url (method url-rule form)
    (:arity 3)
  `(progn
     (add-route ,(intern "*APP*" *package*)
                (url->routing-rule ,method ,url-rule ,form))
     ,form))

ほとんど通常のマクロと同じですが、:arity で、いくつの要素を後に取るかを指定できます。

意味論としてのcl-annot

さて、cl-annotの特徴を一通り述べましたが、それでもまだなんでcl-annotなのかというのはわからないかもしれません。別にアノテーションでなくても、Lispならマクロを使えばいくらでもできます。というか、リードマクロを結局マクロに展開しているだけなので、マクロを直接使うのも得られる結果は同じです。

たとえば、Cavemanの @url は 以下のような define-route というマクロを提供してもいいわけです。

(define-route member-profile "/:member/:id" (params :request-method :GET)
  (render "profile.html" params))

ただ、このコードは凡庸なだけでなく、新しいマクロを持ち込むことによって読み手を混乱させ、新しいフォームの使い方を覚える必要があります。

一方、@urlを再掲します。

@url GET "/:member/:id"
(defun member-profile (params)
  (render "profile.html" params))

@urlアノテーションのメリットは、通常のdefunを使えるということです。@urlも効果としてはマクロには違いないのですが、defunを使うことで、読み手は新しいマクロを覚える必要もなく、さらにコントローラの正体は単なる関数であり、member-profileという関数が定義されるのだということが自明になります。

@exportにしても、defun+みたいなマクロを定義して使っている人もいます。ただ、defun+はexportするだけのdefunなのか、それとも他にも副次的な効果があるのか見ただけでは不明です。

もちろん、実態はただのマクロには違いないのですが、受け取るコードの意味を変更せず尊重するという、書き手の特殊な表現方法がアノテーションなのです。

まとめ

cl-annotを使うと便利とか速いとか、そういったことは全くないのですが、コードの表現方法を1つ提供しているという点で僕は素晴らしいライブラリだと思っています。もっと多くの人に使われていろんなアノテーション定義が出てくると面白いんですけどね。