anaphora: itによる前方参照マクロ集

概要

このページでは、itシンボルで前方参照可能な「アナフォリックマクロ」を集めたanaphoraを紹介します。

アナフォリックマクロとは

おそらくCommon Lisp以外ではほとんど目にすることのない言葉の一つが「アナフォリックマクロ」です(他には「総称関数」「コンディション」などもCommon Lisp固有かもしれません)。

この言葉を広めたのはPaul GrahamのOn Lispに間違いありません。On Lispはマクロに関する決定版のテキストです。私も読みましたし、おすすめできるのでLisp Linksでも紹介しています。

私はCommon Lispを学ぶ前にPerlを知っており、Perlの独特な特殊変数を好んで使っていました。Perlには$_という特殊変数があり、変数(引数)を省略するとこの変数が使われるという機能があります(デフォルト変数)。

アナフォリックマクロはプログラミング的にはPerlの$_に近く、変数の束縛を省略すると、itというシンボルが変数として使われる、という機能を実現します。アナフォラとは前方参照を意味します(On Lispでは「前方照応」と訳しています)。

もっとも、難しく考える必要はなく、itというシンボルはまさに英語のitと同じです。itは文脈に応じて指し示すものが変わりますが、アナフォリックマクロのitも同じで、使用するシチュエーションによって変わります。

Common Lispの代表的なパッケージの一つであるanaphoraはいくつかのANSI標準オペレータのアナフォリック版を提供します。また、単純に「値」をletスペシャルオペレータでitに束縛するa-系マクロだけでなく、「式」をsymbol-macroletスペシャルオペレータでitに束縛するs-系マクロも提供します。シンボルマクロは式をシンボルに束縛するというCommon Lisp秘伝の奥義のようなものですが、混乱しやすいため一般にはあまり使われません。

このページではanaphoraのオペレータを3種類に分けて簡単に紹介します。

なお、anaphoraのオペレータは基本的に全てシンボルを継承して使われます。ASDFでロードし、use-packageもしくはdefpackage:useでシンボルを継承してから以下のサンプルを使ってください。
(asdf:load-system :anaphora)
; => T
(use-package :anaphora)
; => T

なお、エラーが出る場合はこのページの最後でシンボルの競合について解説していますので、そちらを参照して解決してください。

値を束縛するアナフォリックマクロ

まず、anaphoraで最もよく使われるのは第1引数の評価結果(値)をitに束縛するというオペレータです。

例えば、letスペシャルオペレータのアナフォリック版であるaletマクロは以下のように使うことができます。
(alet (+ 1 2)
  (format nil "~a" it))
; => "3"

itというシンボルは表面上未定義(未束縛)に見えますが、aletというアナフォリックマクロの働きによって(+ 1 2)の評価結果である3が束縛されています。そのため、エラーにはなりません。

このようなアナフォリックマクロが複数定義されており、aifawhenが代表的です。全12種類についてはanaphoraのフォルダに入っているREADME.mdを見れば書いてありますので、自分で確認してください。

式を束縛するシンボルマクロ型アナフォリックマクロ

関数とマクロの違いは一般的に「関数は値を返すが、マクロは式を返す」と説明されます。同様にletsymbol-macroletの違いは「前者は値を束縛するが、後者は式を束縛する」と説明できます。

値ではなく式を束縛すると何が嬉しいかというと、setfマクロの第1引数で使うことができるのです。

(defparameter *list* '(1 2 3))
; => *LIST*

(sif (second *list*)
     (setf it nil)
     (setf it t))
; => NIL

*list*
; => (1 NIL 3)

setfマクロの第1引数で使うためには(second *list*)の評価結果である2ではなく、(second *list*)という式そのものを束縛しておく必要があります。そのため、この機能はsymbol-macroletスペシャルオペレータを使ったシンボルマクロでしか実装することができません。

もちろん、このようなシンボルマクロは注意が必要です。特に複数回の評価を不意に行ってしまうため、副作用には十分に注意してください。

以下では乱数の発生という複数評価に弱いシチュエーションを作為的に作り出してみます。
;; 0以上n未満の乱数を生成し、表示して返す関数を定義しておく
(defun print-random (n)
  (print (random n)))
; => PRINT-RANDOM

;; 何回か試す
(sif (print-random 2)
  it)
; 
; 1 
; 1 
; => 1

(sif (print-random 2)
  it)
; 
; 0 
; 0 
; => 0

;; 異なる数が表示されることがあるが、
;; この時までミスに気づかない
(sif (print-random 2)
  it)
; 
; 0      ;; sif 直後の条件分岐による1度目の乱数生成(表示分)
; 1      ;; it すなわち (print-random 2) の再評価による乱数生成(表示分)
; => 1   ;; it の返り値

anaphoraは有名なので、s-系マクロを見た場合は多くの人が「これはシンボルマクロだ」ということに気づき、注意してソースコードを読むことになります。しかし、シンボルマクロの怖いところはソースコードにあるのではなく、シンボルに束縛される式が副作用を持っていたり、環境に影響されたりする場合です。これはプログラマでは防ぎがたく、ユーザーの手に委ねられているということになります。

a-系アナフォリックマクロでも十分に「黒魔術」的と呼ばれますが、s-系アナフォリックマクロはそれ以上に不可解なバグの原因になり得ますので、うまく使ってください。

なお、こちらも12種類のオペレータが定められていますので、同様にREADME.mdを参照してください。

asif: ミックス系アナフォリックマクロ

最後にa-系とs-系が合わさったマクロとして唯一asifが定められています。これは、ifthen部分ではletによる値の束縛を、else部分ではsymbol-macroletによる式の束縛を行うというものです。

asifの主な使い道はこうです。まず、汎参照の形でどこかを参照し、その真偽を調べます。真の場合(nil以外の場合)はその値を使いますが、偽(nil)の場合は最初の汎参照を使ってsetfマクロで値を書き換える、というような場合です。

以下でサンプルを示します。
(defparameter *list* '(1 2 3))
; => *LIST*

;; ここではthen節が使われるので、it = 2
(asif (second *list*)
      it
      (setf it 0))
; => 2

*list*
; => (1 2 3)

(setq *list* '(1 nil 3))
; => (1 NIL 3)

;; ここではelse節が使われるので、it = (second *list*)
(asif (second *list*)
      it
      (setf it 0))
; => 0

;; 値が変わっている
*list*
; => (1 0 3)

itシンボルの衝突

情報技術のことをInformation Technologyと言うので、略してITです。例えば、IT人材の人数などをitというシンボルに束縛しておこうと思って、itというシンボルをすでに使っているときにanaphoraパッケージのロードと継承を行うとエラーになります。

処理系を起動した直後などで試してみてください。

;; 単に処理系でitと打ち込んだだけ(変数の定義も束縛もしていない)
'it
; => IT

(require "asdf")
; => T

;; ここまでは普通にロードできる
(asdf:load-system :anaphora)
; => T

;; 急にエラーになる
(use-package :anaphora)
; *** - (USE-PACKAGE (#<PACKAGE ANAPHORA>) #<PACKAGE COMMON-LISP-USER>): 1 name
;       conflicts remain
;       Which symbol with name "IT" should be accessible in
;       #<PACKAGE COMMON-LISP-USER>?
; The following restarts are available:
; ANAPHORA       :R1      #<PACKAGE ANAPHORA>
; COMMON-LISP-USER :R2    #<PACKAGE COMMON-LISP-USER>
; ABORT          :R3      Abort main loop

しかし、怯える必要はありません。Common Lispのパッケージシステムとコンディションシステムは非常に柔軟です(第9章「コンディション」と第11章「パッケージ」を参照してください)。

処理系によってエラーメッセージは違うと思いますが、要は"IT"という名前のあるシンボルに競合が発生しているからどのパッケージのを使うか選んでね、ということです。anaphoraを使う場合は必ずanaphora:itを使うことになるので:r1を選択するのがいいでしょう。間違えてanaphoraをロードしてしまったということや、anaphoraのitは常にanaphora:itという形式でアクセスするので継承はやっぱり不要です、という場合は:r2を選択します。

ここでは:r1を選んでみます。
:r1
; => T

返り値のtはコンディションシステムが発動する直前の式であるuse-packageの返り値です。つまり、エラーは発生したものの、何事もなかったように正常に動作を継続することができたのです。

試しに、itというシンボルがどのパッケージのものか、確認してください。
(symbol-package 'it)
; => #<PACKAGE ANAPHORA>

これは仕様入門第11章「パッケージ」で説明したRestartによる解決策です。一般にはエラーの通知(デバッガの起動)は避けたいはずなので、shadowing-importを使って明示的にanaphoraのitを継承するか、shadowを使って明示的にanaphoraのitを除外するかが推奨されます。

それぞれ、処理系を起動した直後で試してください。まずはanaphoraのitを継承する場合です。
(require "asdf")
; => T

'it
; => IT

(asdf:load-system :anaphora)
; => T

;; 明示的にanaphoraのitを継承する
(shadowing-import 'anaphora:it)
; => T

;; エラーは通知されない
(use-package :anaphora)
; => T

;; anaphoraのitが優先されている
(symbol-package 'it)
; => #<PACKAGE ANAPHORA>

次はCOMMON-LISP-USERit(自分で打ち込んだシンボル)を優先する場合です。
(require "asdf")
; => T

(asdf:load-system :anaphora)
; => T

'it
; => IT

;; 明示的に打ち込んだitを優先する
(shadow 'it)
; => T

;; エラーは通知されない
(use-package :anaphora)
; => T

;; 打ち込んだitが優先されている
(symbol-package 'it)
; => #<PACKAGE COMMON-LISP-USER>

anaphoraは自分で使うことは少なくても、定番のライブラリが仕様していることが多いため、結果的に必要になる場合があります。最も注意が必要なのはこの節で述べたitの衝突と、シンボルマクロの扱いです。その点を注意すれば非常に面白い機能ですので、どんどん使ってみてください。

0 件のコメント :

コメントを投稿