Posted 2015-07-07 22:15:22 GMT

ユーザーがマクロで構文を定義できるLISPのような言語では、どれがマクロでどれが関数か分からなくて混乱する、という話がありますが実際の所はどうなのでしょうか。

特にLispの場合は、構文の見た目は関数とマクロで同じなので、区別が付かずLispを修得する上での難関などとも云われますが、果してそうなのでしょうか。

自分はLispを始めた当初から混乱することは全くありませんでした。

実際の所、混乱しない人は、どういう風にS式を区別しているかというと、結局のところ、

(foo x y z) (foo-macro x y z)

という感じです。

単純にこれだけのことです。

マクロ命名の慣習を調べてみる

-macro というのは極端ですが、構文の用途に応じてなんとなくの慣習が数種類あるだけで結局同じです。

実際の所はどうなのか、命名規則がひどいと評判のCommon Lispの標準関数で調べてみます。

マクロ以外に特殊形式と呼ばれるシステム定義のマクロのようなものが116ありますが、ざっと分類すると13位に分けられそうです。

マクロの分類

では、命名規約によって分類された構文を眺めてみましょう。

定義系 def-

(defun defstruct defmacro define-method-combination defparameter defmethod defconstant define-symbol-macro define-condition define-modify-macro defvar deftype defpackage define-compiler-macro defgeneric defclass define-setf-expander defsetf)

def なんとかや、 define なんとかで始まる構文です。

Lisp方言では大体共通です。

do系

(dolist do-symbols do do-external-symbols do-all-symbols dotimes do*)

副作用が主で繰り返し構文に多く付けられる接頭辞です。MIT系のLisp方言では大体共通です。

束縛系 -bind let

(flet let macrolet let* symbol-macrolet) (restart-bind destructuring-bind handler-bind)

束縛系の構文付く接尾辞です。接頭辞にletというパタンもあります。

with- 系

(with-package-iterator with-condition-restarts with-standard-io-syntax with-simple-restart with-output-to-string with-open-stream with-compilation-unit with-hash-table-iterator with-slots with-input-from-string with-accessors with-open-file)

束縛系に近いですが、何かのリソースを扱い構文を抜けたら適切に後処理、というようなパタンに使われる名前です。

MIT系のLisp方言では大体共通。

Schemeなどでまれに、 with- という名前で関数だったりすることがありますが、個人的には気持ち悪いです。関数の場合は、 call-with- にして欲しい。

多値系

(multiple-value-prog1 multiple-value-setq multiple-value-bind multiple-value-call multiple-value-list)

Common Lisp固有な感じですが、多値を扱うものに付いている接頭辞です。

セッター系 -F

(setf shiftf setq psetf incf decf rotatef psetq)

(pushnew push pop pprint-pop)

中置記法の言語でいう左辺値的な場所に値をセットする構文です。 謎の接尾辞 F の由来は field という話もありますが、 field も怪しいなと個人的には考えています。

Q の場合は、Quoteが由来ですが説明は省きます。

また、Scheme系だと ! で統一されています。

参照(Fの由来): A LISP machine with very compact programs

push と pop が F が付かない例外的な名前になっていますが、この辺りは暗記する他ないかもしれません。

多方向分岐系 -case

(ecase case handler-case ccase ctypecase etypecase restart-case typecase)

多方向分岐にはcaseが接尾辞に使われます。これもMIT系Lisp共通。

逐次実行系 prog-

(prog progv prog* prog2 progn prog1)

prog という謎の命名になっていますが、歴史的事情によります。

構文機能もちょっとごちゃごちゃしていますね。

prog 系で新しい構文が作られることは少ないですが統一感がなく、構文を派生させにくいからかもしれません。

return系

(return return-from)

ブロックから抜ける構文です。他の言語系でも一般的かと思います。

分岐系

(unless eval-when if and when cond or)

これも他の言語系と一緒かなと思います。 cond がLisp以外では馴染みがないかもしれません。

その他

(catch lambda trace locally step unwind-protect formatter ignore-errors loop-finish declaim function labels nth-value load-time-value quote untrace pprint-logical-block time remf call-method go tagbody pprint-exit-if-list-exhausted block throw assert the print-unreadable-object loop in-package check-type)

残りはこんな感じになりますが、 catch と throw であったり、 tagbody と go であったり制御構文系のもの、 quote 、 function のように評価をエスケープするもの、 the のように注釈するもの位です。

残りは、「あ、これはマクロだったのかー」というような感じなので、やはり命名規約で考えている気がします。

Common Lispの標準関数を眺めてみた感じは以上ですが、大体関数では実装できないパタンのカタログになってもいることが分かります。

13に分類しましたが、実質的には、def、let/bind、do、with-、-f系の5つ位を知っていれば、実際的には殆どカバーできると思います。

命名規約と構文構造の統一性

マクロの場合、関数と違って、どこが評価されて評価されないのかはマクロの設計者の考え次第で任意です。

下手をすれば、読み手にとってはそれが負担になる訳ですが、慣習には評価される/されない場所の設計もまた組込まれています。

構文のインデントについて

構文のインデントも割と重要な所ですが付属的な所があります。

構文のインデントによってマクロかどうかが分かることもあります。

ユーザー定義のライブラリを眺めてみる

さて、Common Lispの標準の慣習は大体分かりました。

次に、Lispマクロの実体を知らない人からは、ユーザーが書きたい放題構文を作るので、何もかもが混沌としている、などといわれがちなユーザーライブラリを眺めてみましょう。

alexandriaの場合

ユーティリティのライブラリで人気のalexandriaのマクロを上記のように腑分けしてみると下記のような感じです。

(when-let when-let* if-let) (doplist) (multiple-value-prog2) (remove-from-plistf nreversef removef maxf reversef coercef minf unionf delete-from-plistf deletef ensure-functionf nconcf appendf nunionf) (with-output-to-file with-input-from-file with-unique-names with-gensyms) (define-constant) (destructuring-ccase destructuring-case unwind-protect-case destructuring-ecase) (eswitch named-lambda nth-value-or whichever ensure-gethash switch cswitch ignore-some-conditions once-only xor)

大体、命名規約で何がどんな感じなのか想像できるのではないでしょうか。

fare-utilsの場合

他、fare-utilsの場合も、大体どんな構文かは想像が付きます。

(if-testing if-bind when-bind) (dolist-with-rest) (multiple-value-quote) (funcallf aif def*setf post-incf post-decf mvsetq append1f) (with-buffered-file-contents with-user-output-file with-input with-nesting) (define*-setf-expander define-package-mix def*constant def*class def*package def*var def*struct def*method defsubst define-values-modify-macro def*fun def*parameter define-values-post-modify-macro define*-symbol-macro define*-exporter def*type define*-modify-macro define*-condition defconstant* define*-method-combination def*generic define*-compiler-macro defun-inline define-exporter def*macro define-abbrevs define-post-modify-macro) (vector-bind mvbind) (copy-symbol-value mvprog1 dbg-time acond msg hashmacro mvquote propmacros eval-once mvcall copy-symbol-function test-only ttest cond2 acond2 ttest* aif2 if2 the* evaluating-once exporting-definitions tsen test-forms make-single-arg-form mvlist accessors-equal-p ensure-symbol-exported dbg nest test-form ensure-symbols-exported eval-now xtime ensure-symbols-exported* mapmacro propmacro ndolist-with-rest declare-type hashmacros declaim-type)

まとめ

以上、Lispを書いている人の、規約があるから混乱することはないなーという感覚をCommon Lispの標準関数を分類してみることで説明してみました。

コードを読むのは勿論ですが自作の構文でも、このような慣習を尊守して書いていることが殆どで何も特別なことはありません。

なお、今回の抽出にはこんな感じの関数を定義して使いました。

( pkg ) do ' ( ) ( ^f ' ( ) ) case ' ( ) ( ^misc ' ( ) ) ( ^with ' ( ) ) ( ^def ' ( ) ) ( ^bind ' ( ) ) prog ' ( ) ( ^mv ' ( ) ) let ' ( ) push ' ( ) if ' ( ) return ' ( ) do-external-symbols ( s pkg ) when and fboundp s or macro-function s special-operator-p s cond "^DEF" string s ppcre:scan ( ^def s ) "(^IF(-|$)|^COND$|WHEN|UNLESS|^AND$|^OR$)" string s ppcre:scan if s "^DO" string s ppcre:scan do s "^RETURN" string s ppcre:scan return s "LET \\ **$" string s ppcre:scan let s "^MULTIPLE-VALUE" string s ppcre:scan ( ^mv s ) and "(F$|Q$)" string s ppcre:scan not "^(IF|REMF)$" string s ppcre:scan ( ^f s ) "^WITH" string s ppcre:scan ( ^with s ) "BIND$" string s ppcre:scan ( ^bind s ) "^PROG" string s ppcre:scan prog s "CASE$" string s ppcre:scan case s "(PUSH|POP)" string s ppcre:scan push s T ( ^misc s ) list if return do let ( ^mv ) ( ^f ) ( ^with ) ( ^def ) case ( ^bind ) prog push ( ^misc ) sb-int:collect defun sort-by-syntax-name

■

