マクロの記述 - Alexandria

概要

このページではCommon Lispの汎用ライブラリであるAlexandriaのマクロに関するオペレータを紹介します。

with-gensymsマクロ: ユニークなシンボルの生成と束縛

ANSI Common Lispではgensymという関数が定められており、どこでも使われていないユニークなシンボルを生成することができます。マクロ定義の中で変数を使うときはこのgensym関数を使うことで変数の予期せぬ誤参照を防ぐことができますが、多くの変数を扱う時には一つ一つ(gensym)してその返り値を束縛するため、記述が冗長になりがちです。

そこでAlexandriaには複数のユニークなシンボルを生成して束縛するマクロが用意されています。それがwith-gensymsマクロで、おそらくAlexandriaのユーティリティの中でも定番中の定番と言えるオペレータになっています。
(shadowing-import 'alexandria:with-gensyms)
; => T
Alexandriaのユーティリティを使うときにはalexandria:with-gensymsというようにパッケージ名をつける必要がありますが、マクロの記述はコード量を減らすために使われるため、よく使うと考えられるオペレータはインポートしてしまうのが良いでしょう。shadowing-importについては仕様入門の第11章「パッケージ」を参照してください。

実際にユニークな変数を使用するときは、単に第1引数のリストにそのシンボルを列挙します。
(with-gensyms (x y)
  (format t "(x, y) = (~s, ~s)~%" x y))
; (x, y) = (#:X9115, #:Y9116)
; => NIL
シャープコロン(#:)は「インターンされていないシンボル」を示すリーダーマクロです。ここではxyという変数を使っているように見えますが、実際にはユニークなシンボルに置き換わっているのが分かります。

実際にマクロの中で使用する場合は以下のようになります。

例えば、代表的なswapマクロの定義は以下の通りです。
(defmacro swap (x y)
  (let ((temp (gensym)))
    `(let ((,temp ,x))
       (setf ,x ,y)
       (setf ,y ,temp))))
ここでwith-gensymsを使用すると、以下のように記述することができます。
(defmacro swap (x y)
  (with-gensyms (temp)
    `(let ((,temp ,x))
       (setf ,x ,y)
       (setf ,y ,temp))))
使用する一時変数の数が多い場合には特に便利ですので、積極的に使ってください。

once-onlyマクロ: 一度だけの評価を保証するマクロ定義

マクロの定義で注意が必要なのは、不用意な変数束縛だけではありません。評価の回数も注意が必要です。

例えば、引数の値をCARCDRにセットしたコンスを返す関数cons1を定義します。
(defun cons1 (x)
  (cons x x))
; => CONS1
この関数は、どのような引数でも正常に動作します。
(cons1 5)
; => (5 . 5)

(cons1 (random 10))
; => (7 . 7)

ところが、同じ動作をマクロで実装しようとすると、乱数などの際に誤作動が生じます。
(defmacro cons1 (x)
  `(cons ,x ,x))
; => CONS1

(cons1 5)
; => (5 . 5)

(cons1 (random 10))
; => (5 . 8)
これは、5という定数の場合は何度評価しても5ですが、(random 10)という式は評価するために10未満の擬似乱数を発生させるため、値が変わってしまうのです。

この問題はマクロ定義の中でAlexandriaのonce-onlyマクロを介することで解決できます。
(shadowing-import 'alexandria:once-only)
; => T

(defmacro cons1 (x)
  (once-only (x)
    `(cons ,x ,x)))
; => CONS1

(cons1 (random 10))
; => (6 . 6)
複数回の評価を避けたい場合には便利ですので活用してください。

parse-body関数: 定義本文の解析

あまり使うことはないと思いますが、関数の定義をコードではなくデータとして解釈するオペレータも用意されています。

例えば、以下のexpt2関数定義をデータとしてパースしてみます。
(defparameter func
  '(defun expt2 (x)
    (declare (type real x))
    "Equal to (expt x 2)."
    (expt x 2)))
; => FUNC

(cdddr func)
; => ((DECLARE (TYPE REAL X)) "Equal to (expt x 2)." (EXPT X 2))

(alexandria:parse-body (cdddr func)
                       :documentation t)
; => ((EXPT X 2)) ;
;    ((DECLARE (TYPE REAL X))) ;
;    "Equal to (expt x 2)."
関数の定義本体部分をAlexandriaのparse-body関数に渡すと、定義・宣言・ドキュメント文字列の3つの多値が返されます。

destructuring-caseマクロ: ラムダリスト形式のマッチング

Alexandriaには簡易的なマッチングオペレータも付属しています。ANSI Common Lispで仕様として定められているcaseマクロ(第5章「データと制御フロー」)とdestructuring-bindマクロ(第3章「評価とコンパイル」)を組み合わせたdestructuring-caseマクロです。

このマクロはなんでもマッチングできるわけではなく、基本的にはラムダリスト形式の式、つまり関数の引数定義に用いるような形式でのマッチングを行います。
(defun match (v)
  (destructuring-case v
    ((:foo x y) (format nil "Arguments: (x = ~s, y = ~s)" x y))
    ((:bar &key x y) (format nil "Keywords: (x = ~s, y = ~s)" x y))
    ((t &rest rest) (format nil "Unkown: ~s" rest))))
; => MATCH

(match '(:foo 1 2))
; => "Arguments: (x = 1, y = 2)"

(match '(:bar :y 1 :x 2))
; => "Keywords: (x = 2, y = 1)"

(match '(:something 1 2))
; => "Unkown: (1 2)"
キーワード引数のようなマッチングも行うことができるので、リスト内の順序に影響されにくいマッチングを行うことが可能です。

0 件のコメント :

コメントを投稿