common-lispにおけるpackageとsymbolとintern
Lisp で作る Scheme まとめ : セマンティックウェブ・ダイアリー
最近common-lispの単体テストをいろいろと書いているのですがなかなかスッキリと書けません。いろいろ試行錯誤する中で、ちょっと大事なこと?に気づいたので記事にしてみます。
初心者が初心者の方に向けて書いている内容となります。当たり前だろ!と言う方には向かない記事ですのであしからず。。。次の記事に行ってくださいませ。
別のパッケージにある超便利な関数を使いたいのだが。。。
説明を簡単にするため、超簡単なサンプルを改変していく感じで進めたいと思います。
cho-benri関数が :pkg1 というパッケージにあります。自分は :cl-user にいて、cho-benri関数を利用したいと思うという設定です。。。
cho-benri関数は、リストを受け取り 'b というシンボルを見つけると、'bbb に置き換えてくれる超便利な関数です。(これはあくまでも設定ですので、すみません。)
(defpackage :pkg1
(:use :cl)) ; => #<PACKAGE "PKG1">
(in-package :pkg1) ; => #<PACKAGE "PKG1">
(defun cho-benri (list)
(cond ((null list) nil)
((eq 'b (car list))
(cons 'bbb
(cho-benri (cdr list))))
(t (cons (car list)
(cho-benri (cdr list)))))) ; => CHO-BENRI
(in-package :cl-user) ; => #<PACKAGE "COMMON-LISP-USER">
(pkg1::cho-benri '(a b c)) ; => (A B C)
えっ?ってなりませんか?私はなります。そうしないと話が続きませんので。。。
こちらとしましては、'(a b c) が '(a bbb c) になるのを期待するわけですが、そうはなりませんでした。
これは、cho-benri関数内、eq の bと、(pkg1::cho-benri '(a b c)) の b は それぞれのパッケージのシンボルであり、別物であるためです。
比較の仕方が悪いのか?
初心者としましては、eq での比較が適してないのかな??と思うわけです。それではいくつか試してみます。
(eq 'pkg1::b 'cl-user::b) ; => NIL
(eql 'pkg1::b 'cl-user::b) ; => NIL
(equal 'pkg1::b 'cl-user::b) ; => NIL
(string= (symbol-name 'pkg1::b)
(symbol-name'cl-user::b)) ; => T
シンボルの比較は全て偽が返ります。文字列にすればなんとか真が得られます。が、これでいいのか??と疑ってしまいます。
文字列で比較してみる
疑心暗鬼になりながらも、まあやってみましょうということで、超便利関数の第二版の登場です。eq を string= に変更します。
(in-package :pkg1) ; => #<PACKAGE "PKG1">
(defun cho-benri (list)
(cond ((null list) nil)
((string= (symbol-name 'b)
(symbol-name (car list)))
(cons 'bbb
(cho-benri (cdr list))))
(t (cons (car list)
(cho-benri (cdr list)))))) ; => CHO-BENRI
(in-package :cl-user) ; => #<PACKAGE "COMMON-LISP-USER">
(pkg1::cho-benri '(a b c)) ; => (A PKG1::BBB C)
えっ!PKG1::BBBってなんやねん!私は今〜、cl-userにいるのだから、cl-user::b を返してほしいのだが。。。
試行錯誤の末
しんどくなってきたので少し飛躍しますが、以下のプログラムをご覧ください。
:pkg1 に intern-test関数があり、インターンしてくれます。この関数を :cl-user パッケージから :cl-userの シンボル 'bbb を引数として渡して、インターンしてもらいます。
ではどうぞ。
(in-package :pkg1)
(defun intern-test (symbol)
(intern (symbol-name symbol))) ; => INTERN-TEST
(in-package :cl-user)
;;;; こちらの結果を予測できますか?
(eq (pkg1::intern-test 'bbb) 'cl-user::bbb)
; 答えは
; => T
(pkg1::intern-test 'bbb) ; => BBB
; :INTERNAL
この挙動、違和感がありませんか?'bbb がインターされるのはどちら様のパッケージ様になるのでしょうか???
ここはひとつ、hyperspec様で確認してみます。
Function INTERN
Syntax:
intern string &optional package => symbol, statusArguments and Values:
string---a string.
package---a package designator. The default is the current package.
symbol---a symbol.
status---one of :inherited, :external, :internal, or nil.
hyperspec様がおっしゃるには。。。インターンされる先のパッケージ様はカレントパッケージ!*package* 様 なのです!!
他の言語ではシンボルなんてほとんど意識させないわけですよ。シンボルのインターンなんて何に使うのか???って感じでよくわからなかったのですが。。。
この挙動が今回の問題にはうまく当てはまりそうな気がしてきました。
閑話休題:インターンしてみる
超便利関数も第三版になります。さてうまく動いてくれるでしょうか?
もともと超便利関数は再帰関数ですので、シンボルのインターンは1回で済ませたいです。なので let でそれぞれを束縛します。そして let の中で labels を使わせてもらい、再帰関数を束縛します。
(in-package :pkg1)
(defun cho-benri (list)
(let ((old (intern (symbol-name 'b)))
(new (intern (symbol-name 'bbb))))
(labels
((%cho-benri (list)
(cond ((null list) nil)
((eq old (car list))
(cons new
(%cho-benri (cdr list))))
(t (cons (car list)
(%cho-benri (cdr list)))))))
(%cho-benri list)))) ; => CHO-BENRI
(in-package :cl-user)
(pkg1::cho-benri '(a b c)) ; => (A BBB C)
intern様のおかげで、別のパッケージに定義されている関数の中のシンボルを、カレントパッケージのシンボルとしてローカル変数に束縛することができました。結果、入と出においてもカレントパッケージのシンボルとして扱うことができました。
少しひねくれた想定
ちょっと意地悪ですが、超便利関数に先程と同様に cl-user のシンボルを渡します。:pkg2 パッケージを新しく作り、私は今〜、そこにいます。超便利関数第三版の挙動は以下の通りです。
(defpackage :pkg2 (:use :cl)) ; => #<PACKAGE "PKG2">
(in-package :pkg2)
(pkg1::cho-benri '(cl-user::a
cl-user::b
cl-user::c
))
;; =>
;; (COMMON-LISP-USER::A
;; COMMON-LISP-USER::B
;; COMMON-LISP-USER::C)
やっぱりそうですよね、:cl-user の '(a b c) になっちゃいました。できましたら `(a bbb c) が欲しかった。。。
これは 'cl-user:b を引数で渡しておりますが、超便利関数内の eq では カレントパッケージであるところの 'pkg2::b と比較してしまい、合致しなかった。。。ということであります。
ここで再びhyperspecを確認してみます。
Function INTERN
Syntax:
intern string &optional package => symbol, statusArguments and Values:
string---a string.
package---a package designator. The default is the current package.
symbol---a symbol.
status---one of :inherited, :external, :internal, or nil.
はい!引数 packageに、みんな大好き、package desingatorを渡せるじゃないか!!!これでカレントパッケージ固定ではなくて、好きなパッケージを選んで比較・出力できるようになるんじゃないでしょうか。
他のパッケージから他のパッケージにある関数を呼び出して他のパッケージのシンボルを返してもらう
見出しが長いです。さて超便利関数第四版はこちら。
引数オプションで *package* をデフォルトとして、*package* を上書きしてます。スペシャルなアレです。
(in-package :pkg1)
(defun cho-benri (list &optional (*package* *package*))
(let ((old (intern (symbol-name 'b) *package*))
(new (intern (symbol-name 'bbb) *package*)))
(labels
((%cho-benri (list)
(cond ((null list) nil)
((eq old (car list))
(cons new
(%cho-benri (cdr list))))
(t (cons (car list)
(%cho-benri (cdr list)))))))
(%cho-benri list)))) ; => CHO-BENRI
(in-package :pkg2)
(pkg1::cho-benri '(cl-user::a
cl-user::b
cl-user::c)
(find-package :cl-user))
;; =>
;; (COMMON-LISP-USER::A
;; COMMON-LISP-USER::BBB
;; COMMON-LISP-USER::C)
結果をみても、あっているんだか、間違っているんだかよくわからなくなってきましたが、これは要件を満たしています。やったね!
まとめ
ここまでやればもういいでしょう。。。以上、インターンの使い方のほんの一例でした。パッケージを跨いでいろいろやるときにこんなことを意識されてみてはいかがでしょうか?
おまけ
ここは読まれなくてもいいと思います。もう十分だと思います。
さて、シンボルを置き換える前と置き換えた後で、違うパッケージのシンボルを。。。。なんてのもできます。超便利関数第五版です。
キーワード引数をつかって、old と new のそれぞれのパッケージを指定できるようにしました。デフォルトはカレントパッケージです。
(in-package :pkg1)
(defun cho-benri (list &key (old-package *package*)
(new-package *package*))
(let ((old (intern (symbol-name 'b) old-package))
(new (intern (symbol-name 'bbb) new-package)))
(labels
((%cho-benri (list)
(cond ((null list) nil)
((eq old (car list))
(cons new
(%cho-benri (cdr list))))
(t (cons (car list)
(%cho-benri (cdr list)))))))
(%cho-benri list)))) ; => CHO-BENRI
:cl-user::b を :pkg2::bbb に置き換えることができるか?今は :pkg2 にいるので、:old-package に :cl-user を指定します。
(in-package :pkg2)
(pkg1::cho-benri '(cl-user::a
cl-user::b
cl-user::c)
:old-package (find-package :cl-user))
;; =>
;; (COMMON-LISP-USER::A
;; BBB
;; COMMON-LISP-USER::C)
':pkg1::b を 'cl-user::bbb に置き換えます。:old-package、:new-package をそれぞれ指定してみます。
(in-package :pkg2)
(pkg1::cho-benri '(cl-user::a
pkg1::b
cl-user::c)
:old-package (find-package :pkg1)
:new-package (find-package :cl-user))
;; =>
;; (COMMON-LISP-USER::A
;; COMMON-LISP-USER::BBB
;; COMMON-LISP-USER::C)
ということで、おそらく超基本的なことがらなのでしょうが、また、どこかできっちり説明されていたことなのでしょうが、理解不足から実践においてかなり混乱・間違った解決をしてきたように感じます。
何かのお役に立てば幸いです。長らくお付き合いいただき、ありがとうございました。