読者です 読者をやめる 読者になる 読者になる

アナフォリックマクロとpacakge(2)

common lisp

半年くらい前に書いた「アナフォリックマクロとpacakge」の続き。この方法だとダメなケースを発見したので、その改善策(?)を考えてみる。

以前の方法

以前の方法は、アナフォリックマクロの定義と使用が別パッケージに分かれる場合に、アナフォリック変数のインターンを使用側パッケージで行う、というようなものだった。

(defpackage :util
  (:use :common-lisp)
  (:export a.when))
(in-package :util)

(defmacro a.when (expr &body body)
  `(let ((,(intern "IT") ,expr))   ; ITシンボルは、マクロ展開時に使用側パッケージにインターンされる
     (when ,(intern "IT")         
       ,@body)))

この方法の利点は前回書いた通りなのだが、a.whenマクロを使用する関数がインライン宣言されており*1、その関数がさらに別のパッケージで使用されている場合、アナフォリック変数(上の場合は'it')が見つからないというエラーが出てしまう。

;;;;;;;;;;;;;;;;;;;;
;;;; user1パッケージ
;;;; a.whenマクロを使ったインライン関数を定義
(defpackage :user1
  (:use :common-lisp)
  (:import-from :util a.when)
  (:export inline-fn))
(in-package :user1)

(declaim (inline inline-fn))

(defun inline-fn (x)
  (a.when x
     (list :x it)))   ; <- この'it'は、user1::it

;;;;;;;;;;;;;;;;;;;;
;;;; user2パッケージ
;;;; user1:inline-fnを使った関数を定義
(defpackage :user2
  (:use :common-lisp)
  (:import-from :user1 inline-fn))
(in-package :user2)

> (defun fn (y)
    (inline-fn y))  ; <- この場所でinline-fnが展開される
;;; 上の式(関数定義)を評価しようとすると、以下のようなエラーが出る ;;;
; in: LAMBDA NIL
;     (USER1:INLINE-FN USER2::Y)
; --> BLOCK UTIL:A.WHEN LET WHEN IF PROGN 
; ==>
;   (LIST :X USER1::IT)
; 
; caught WARNING:
;   undefined variable: USER1::IT   ; user1::it変数は定義されていない
; 
; compilation unit finished
;   Undefined variable:
;     USER1::IT
;   caught 1 WARNING condition

;;;;;;;;;;;;;;;
;;; fn関数の展開過程
; 1] inline-fnが展開される
(defun fn (y)
  (inline-fn y)) 

; 2] a.whenマクロが展開される
;    ※ この時に(intern "IT")も評価される
(defun fn (y)
  (a.when y
    (list :x user1::it)))

; 3] 結果
(defun fn (y)
  (let ((user2::it y))
    (when user2::it 
      (list :x user1::it))))  ; <- user1::itは未定義

インライン宣言された関数内の'it'は、その場で評価されるので'user1::it'となるが、a.whenマクロ内の(intern "IT")は、インライン関数が実際に展開される時まで評価されないので、'user2::it'となってしまっている。
数日前に、この問題に遭遇した時は、とりあえず関数のインライン宣言を外すことで対処した。

改善策(?)

で、この問題の改善案。
今回および前回挙げた問題はいずれも(?)、'it'シンボルが属するパッケージがコロコロ変わってしまうことに関係していた。
それならば、キーワードに対するキーワードパッケージのように、アナフォリック変数専用のパッケージを作成してしまえば良いのではないかと思う。
後は、アナフォリックマクロの定義者・使用者の双方が、そのパッケージから変数(シンボル)を参照するようにすれば、パッケージに関する問題は起こらなくなるはず。

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; アナフォリック変数用のパッケージ
(defpackage :anaphoric
  (:use :common-lisp)
  (:nicknames :a)
  (:export it))   ; 'it'をエクスポート

;;;;;;;;;;;;;;;;;;;;;;;
;;;; a.whenの新しい定義
(defpackage :util
  (:use :common-lisp)
  (:export a.when))
(in-package :util)

(defmacro a.when (expr &body body)
  `(let ((a:it ,expr))  ; anaphoric:it変数(シンボル)を使用する
     (when a:it
       ,@body)))

;;;;;;;;;;
;;;; user1
(defpackage :user1
  (:use :common-lisp)
  (:import-from :util a.when)
  (:import-from :anaphoric it)  ; anaphoric:itをインポート
  (:export inline-fn))
(in-package :user1)

(declaim (inline inline-fn))

(defun inline-fn (x)
  (a.when x
     (list :x it)))   ; <- この'it'は、anaphoric:it

;;;;;;;;;;
;;;; user2
(defpackage :user2
  (:use :common-lisp)
  (:import-from :user1 inline-fn))
(in-package :user2)

> (defun fn (y)
    (inline-fn y)) 
--> FN   ; 特に問題無し

> (fn 10)
--> (:X 10)

当面は、この方法で試してみよう。

*1:インライン関数ではなく、マクロでも可