Common Lisp の package について

編集履歴

はじめに

この記事では, プログラミング言語Common Lispのpackageと呼ばれる概念について書きます.

人に向けて書いている風の備忘録なので悪しからず.

というような人向けになっています. 処理系や仕様としての実装の詳細には立ち入らない予定です.

また, asdfを用いた依存関係の解決などについてもここでは触れません. (別のページに書く可能性はあります.

また, この記事を書くに当たり用いている処理系は, SBCL 2.0.6です.

名前空間

プログラミング言語一般の話として, 名前空間と呼ばれる概念があります.

これはコード中で用いられるある名前(コンパイラやインタプリタが読み取る文字列)に対して, その名前が何を指しているのかという対応付けのことです.

複数の名前空間を扱えるプログラミング言語では, この対応付け(名前空間)を分けることで, 不必要に冗長な命名規則(いつでもどこでも接頭辞が付くような書き方など)を除きつつ, 名前の衝突を避けることが出来ます.

ここで名前の衝突というのは, プログラムが扱う別々の対象について, ある同じ名前がつけられてしまうことを言います.

この時, 名前から一意にプログラムが扱う対象を引くことができなくなるため, 困ります. 例えば, 意図せず関数が再定義されたり, プログラムがクラッシュしてしまうことがあるかもしれません.

多くのプログラミング言語ではこれを回避するために, 再定義に関するエラーを出したり, 複数の名前空間を持つことで一見同じ名前であっても, 名前空間Aのfooはbarを指して名前空間Bのfooはbazを指すというようなことが出来るようになっています.

symbolとpackage

Common Lispで, 読み取り機がコードを読み取って, コードの表現を値にするまで, 例えば,

CL-USER> pi
3.141592653589793d0

これは, piという名前から値3.141592653589793d0を引いています. これを理解するためには,

という2つの対応付けのことを考えないといけません.

この文章のテーマであるpackageというのは, 1つ目の対応付け(コード上の文字列からsymbolを引くということ)に関連します.

(2つ目の対応付けについては, ざっくりとした言い方をすると, Common Lispでは関数名や変数名がsymbolで, symbolというオブジェクト(データ)が関数や変数を指し示すためのものとなります.)

symbolとpackageの関係からsymbolに次の3つの状態があると考えると分かりやすいです.

参照できるというのは, その名の通りあるsymbolを参照する方法がある状態であり, メモリ上のどこかにそのsymbolがあるという程度のことで, これだけではあまりpackageとは関係がありません.

どのようにしてsymbolを参照できるのかということに, packageが関わってきます.

current package

pacakgeとsymbolの関係の前に, current packageについて説明しておきます.

ある時点で, 一つのpackageがcurrentです. このpackageをcurrent pacakgeと呼びます. これは, *package*変数を評価した時の値です.

あるpackage内でsymbolがアクセス出来る. と上で書きましたが, これは, そのあるpackageがcurrent packageである時にpackage prefixなしにsymbolを参照できることを指します. (package prefixについては後述します.)

symbolをpackageに存在させることを, internすると言います.

symbolが最初にinternされるpackageを, symbolのhome packageと呼びます. symbolはこのhome packageの情報を持ちます.

home packageはsymbol-packageで確認できます.

CL-USER> (symbol-package 'pi)
#<PACKAGE "COMMON-LISP">

実は, home packageつまりsymbol-packageで確認できるpackageを変更することが出来ますが, ここではそのことは考えません.

symbolは複数のpackageにinternされますが, symbolのhome packageは高々一つです.

また, symbolが最初にinternされると, packageにsymbolが存在する(is present)状態になります.

これが上で挙げた3つ目の状態(あるpackageにsymbolが存在している)です.

また, 言い換えになりますが, symbolがアクセス可能なpackageでは, symbolは名前(これはコード上の文字列です)で一意に識別出来ます.

package prefix

変数PIの例を見てみます.

CL-USER> pi
3.141592653589793d0
CL-USER> cl:pi
3.141592653589793d0
CL-USER> cl::pi
3.141592653589793d0
CL-USER> cl-user::pi
3.141592653589793d0

これら, pi, cl:pi, cl::pi, cl-user::piは, 全て同じsymbolを指しています.

piの前についている, cl:cl-user::等がpackage prefixの例になります.

packageを表す文字列の後に, コロンが1つあるいは2つが続いて, その後にsymbolの名前を表す文字列が続きます.

コロンの数による違いは, 指定するpackageにおいて, symbolがinternalなのかexternalなのかによって動作が変わります.

なお, このコロンのことをpackage markerと呼びます.

internal と external

symbolがあるpackageでアクセス出来るということを話しました.

アクセスできるsymbolは2つの観点からそれぞれ, 2種類の状態があります.

1つ目の2種類は, 次の2つです.

これは, symbolがexternalかどうかという観点です.

あるpackageにおいてsymbolがexternalであるというのは, そのsymbolが, そのpackageが外部に提供するインターフェースの一つであることを指します.

上で書いたPIの例で, cl:piとclというパッケージ名の後にはコロンが一つでしたが, これはCOMMON-LISP packageにおいてsymbol PIがexternalであるからです.

externalなsymbolのみが, パッケージ名を表す文字列のあとにコロン1つで参照できます.

internalなsymbolでも, パッケージ名を表す文字列のあとにコロン2つで参照できますが, コロン1つでは参照できません. externalなsymbolはコロン2つでも参照できます.

current packageであるsymbolがinternalなのかexternalなのかということは, internの第一引数にsymbolの名前, 第二引数を指定しないとすると確認することが出来ます.

CL-USER> (in-package :cl)
#<PACKAGE "COMMON-LISP">
CL> (intern "PI")
PI
:EXTERNAL

in-packageはcurrent packageを切り替えます. また先程から登場しているcl(CL)というのはCOMMON-LISP packageのニックネームです. (ニックネーム機能については後で紹介します.)

既にinternされているsymbolの名前をinternに渡してやると, そのsymbolとsymbolがinternalかexternalかということを返します.

:INTERNALあるいは:INHERITEDが返ってくる場合は, current packageにおいて, そのsymbolはcurrent packageでinternalです. :EXTERNALが返ってくる場合は, current packageにおいて, そのsymbolはcurrent packageでexternalです.

inheritedについては, 後で説明する意味がありますが, このやり方でinheritedと返ってきたときには, そのsymbolはcurrent packageでinternalであることも意味します.

CL-USER> (INTERN "PI")
PI
:INHERITED
CL-USER> (INTERN "PI" :cl)
PI
:EXTERNAL

このように第2引数を指定すると, current package以外のpackageでの扱いについて確認することも出来ます.

present と inherited

2つ目の観点はpackageに存在するsymbolかどうかです.

あるpackageでアクセス可能なsymbolは,

のどちらかです.

あるpackageでアクセス出来るけれども, inheritedというのは別のpackageにsymbolが存在するという状態です.

これの例は, COMMON-LISP-USER packageにおけるPIです.

CL-USER> *package*
#<PACKAGE "COMMON-LISP-USER">
CL-USER> (intern "PI")
PI
:INHERITED

current packageがCOMMON-LISP-USERであって, package prefixなしに参照は出来ますが, internを用いると2つ目の返り値が:INHERITEDとなっています. package prefixなしに参照できる, つまりaccessibleではありますが, このsymbolはCOMMON-LISP-USERというpackageには存在していません.

あとで紹介するuse-package等によって, package(の持つ名前とsymbolの対応)間に継承関係を設定できます.

PIの例場合, package CL-USERがpackage CLを使っているという状態にあって, CLがCL-USERのuse listというものに含まれています.

symbol PIはuse listを通してpackage CLから探されます.

defpackage

symbolとpackageの関係性をざっと説明しました. これらをどのように制御するのかということが, Common Lispにおけるpackage管理になります.

とはいえ, packageを作らないことには始まりません. defpackageを使ってpackageを作ることが出来ます.

以下のようにすると名前がFOOであるpackageが作成されます.

CL-USER> (defpackage :foo)
#<PACKAGE "FOO">

defpackageの第一引数はpackageの名前となるもので, 文字列, 文字, シンボルを使うことが出来ます. これらはstring designator(文字列指定子)と呼ばれます.

また, ここでは使えませんが, これらにpackage自身を合わせたpackageを指定するもののことを, package designator(パッケージ指定子)と呼びます.

defpackageにおいてpackageやsymbolの名前を表すために, #付きのkeyword symbolを用いることがありますが, ここではその流儀を採用しません. (もし気になる方は, 何故そのような流儀があるかを調べてみてください.)

packageの名前は以下のようにpackage-nameで調べることが出来ます. packageはそれ自体objectですが, これを取得するためにはfind-packageを用います.

CL-USER> (defpackage :foo)
#<PACKAGE "FOO">
CL-USER> (find-package :foo)
#<PACKAGE "FOO">
CL-USER> (package-name (find-package :foo))
"FOO"

in-package

current packageを変更するには, in-packageを使います.

CL-USER> (defpackage :foo)
#<PACKAGE "FOO">
CL-USER> (in-package :foo)
#<COMMON-LISP:PACKAGE "FOO">
FOO> cl:*package*
#<COMMON-LISP:PACKAGE "FOO">

SLIMEなどのREPLでは, これまで示してきたようにプロンプトにcurrent packageの名が表示されます.

COMMON-LISP-USERではなくCL-USERと表示されていますが, これはnicknameです.

nicknameを確認するには, package-nicknamesを使います.

CL-USER> (package-nicknames *package*)
("CL-USER")

ここでcurrent packageがpackage FOOの時に, cl:*package*とpackage prefixをつけたのは, このsymbolがpackage FOOでアクセス可能ではないからです.

defpackageでpackageを作成する時に, どんなsymbolがaccessibleになるかということは実装依存ですが, ここで用いている処理系(SBCL)ではどのsymbolもaccessibleになりません.

intern

symbolをpackageにinternしてみます. cl:internを使ってみましょう.

CL-USER> (defpackage :foo)
#<PACKAGE "FOO">
CL-USER> (in-package :foo)
#<COMMON-LISP:PACKAGE "FOO">
FOO> (cl:intern "HOGE")
HOGE
COMMON-LISP:NIL
FOO> (cl:intern "HOGE")
HOGE
:INTERNAL

このように, その名前を持つsymbolがinternされていない時に, cl:internを(第2引数を省略して)使うと, symbolが作成され, current packageにその名前を持つsymbolがinternされます.

この場合は, symbol foo::hogeがpackage FOOにinternされます.

しかし, もっと実用的に出てくるものとして, 関数定義でもsymbolはinternされます.

cl:defunを使って関数を定義してみます.

FOO> (cl:defun piyo () 42)
PIYO
FOO> (cl:intern "PIYO")
PIYO
:INTERNAL

この例ですと, 関数名になるsymbol foo::piyoがcurrent package (package FOO)にinternされます.

その実, symbolは読み取られるとcurrent packageにinternされます.

FOO> 'fuga
FUGA
FOO> (cl:intern "FUGA")
FUGA
:INTERNAL

export

symbolはあるpackageに対してinternalとexternalという2つの種類の状態があると先に紹介しましたが, 直前のinternの説明でsymbolがpackageにinternされてinternal symbolになるところは確認できました.

これらをexternalにするには, cl:exportを使います.

FOO> (cl:export 'hoge)
COMMON-LISP:T
FOO> (cl:intern "HOGE")
HOGE
:EXTERNAL

exportの第一引数にはsymbolのlistを渡すことも出来ます.

FOO> (cl:export '(piyo fuga))
COMMON-LISP:T
FOO> (cl:intern "PIYO")
PIYO
:EXTERNAL
FOO> (cl:intern "FUGA")
FUGA
:EXTERNAL

先に書いたとおり, external symbolはコロンが一つのpackage prefixで参照出来ます.

FOO> (cl:in-package :cl-user)
#<PACKAGE "COMMON-LISP-USER">
CL-USER> (foo:piyo)
42
CL-USER> 'foo:hoge
FOO:HOGE

use-package

先にもPIの例について書きましたが, package CL-USERではin-packagedefunをpackage prefixなしに使っています. つまり, CL-USERでcl:defuncl:in-packageがアクセス可能ということです.

cl:use-packaceを使うと, あるpackageのexternal symbolを全てアクセス可能にすることが出来ます.

CL-USER> (in-package :foo)
#<COMMON-LISP:PACKAGE "FOO">
FOO> (cl:use-package :cl)
T
FOO> (in-package :cl-user)
#<PACKAGE "COMMON-LISP-USER">
CL-USER> 

上の例では, package FOOに, package CL(package COMMON-LISP)のexternal symbol全てがinternされます. そのためin-packageがpackage prefixなしで参照出来るようになりました.

先にも書きましたが, このようにして,アクセス可能になったsymbolはinheritedであるといいます.

これをpackage FOOがpackage CLを使うと言います.

使っているpackageはpacakge-use-listで確認できます.

FOO> (package-use-list :foo)
(#<PACKAGE "COMMON-LISP">)

Common Lisp処理系を立ち上げると, current packageはpackage COMMON-LISP-USERになっています. package COMMON-LISP-USERは, package COMMON-LISPを使っています.

shadow

あるpackageのexternal symbolを別のpackageでアクセス可能にする方法について, use-packageを紹介しました.

この方法では, external symbolを全てアクセス可能にします.

これだけでは, あるpackageを使いたいけれども, そのpackageのexternal symbol全てが必要ではないという場合. 特に, あるsymbolについて同じ名前のsymbolを使いたい時に困ってしまいます. 具体的には, 同じ名前の別の関数を定義したい場合などです.

これまでの例を一度忘れて, package FOO内で関数barを定義したとします.

CL-USER> (defpackage :foo)
#<PACKAGE "FOO">
CL-USER> (in-package :foo)
#<COMMON-LISP:PACKAGE "FOO">
FOO> (cl:use-package :cl)
T
FOO> (defun bar () 1)
BAR
FOO> (export 'bar)
T

この状態で, current packageをpackage CL-USERに戻して, package FOOを使うことにします.

FOO> (in-package :cl-user)
#<PACKAGE "COMMON-LISP-USER">
CL-USER> (use-package :foo)
T
CL-USER> (bar)
1

この時, 関数barを再定義すると, それはsymbol foo:barに結び付けられてしまします.

CL-USER> (defun bar () 2)
WARNING: redefining FOO:BAR in DEFUN
BAR
CL-USER> (bar)
2
CL-USER> (in-package :foo)
#<PACKAGE "FOO">
FOO> (bar)
2

わざとこのようにしたいのならともかくとしても, これではpackage FOOに依存している別のpackageにまで影響を及ぼしてしまいます.

逆に, 何らかのpackageを使う際に, 既にinternされているsymbolと同じ名前のものを取り込もうとするとエラーとなります.

CL-USER> (defpackage :hoge)
#<PACKAGE "HOGE">
CL-USER> (in-package :hoge)
#<COMMON-LISP:PACKAGE "HOGE">
HOGE> (cl:intern "BAR")
BAR
COMMON-LISP:NIL
HOGE> (cl:export 'bar)
COMMON-LISP:T
HOGE> (cl:in-package :cl-user)
#<PACKAGE "COMMON-LISP-USER">
CL-USER> (use-package :hoge)
; ここで以下のエラーが出ます.
; USE-PACKAGE #<PACKAGE "HOGE"> causes name-conflicts in
; #<PACKAGE "COMMON-LISP-USER"> between the following symbols:
;   HOGE:BAR, FOO:BAR
;    [Condition of type NAME-CONFLICT]

前者の場合, package CL-USER内でbarがpackag FOOに存在するfoo:barを指すということが問題なので, 代わりに, package CL-USERに存在するsymbol barを指すように出来ればうまく行きます.

このような時には, shadowを使います.

CL-USER> (defpackage :foo)
#<PACKAGE "FOO">
CL-USER> (in-package :foo)
#<COMMON-LISP:PACKAGE "FOO">
FOO> (cl:use-package :cl)
T
FOO> (defun bar () 1)
BAR
FOO> (export 'bar)
T
FOO> (in-package :cl-user)
#<PACKAGE "COMMON-LISP-USER">
CL-USER> (use-package :foo)
T
CL-USER> (shadow 'bar)
T
CL-USER> (defun bar () 2)
BAR
CL-USER> (bar)
2
CL-USER> (symbol-package 'bar)
#<PACKAGE "COMMON-LISP-USER">
CL-USER> (in-package :foo)
#<PACKAGE "FOO">
FOO> (bar)
1
FOO> (symbol-package 'bar)
#<PACKAGE "FOO">

shadowは, 第1引数に与えられた名前を持つsymbolが, 第2引数で指定するpackage内に存在することを保証する状態にするものです. 第2引数を省略した場合にはcurrent packageになります.

第1引数には名前を指定しますが, 文字列以外にも文字列指定子を用いることが出来ます. (この場合は先に書いたpackageの名前ではなくsymbolの名前として働く.) 文字列で指定すると大文字小文字のことを考えないといけませんが, 例のように, symbolを使うとそのsymbolの普段のコード中の表現を使えるので便利です.

上の場合, symbol barがpackage CL-USERに存在することになるので, (defun bar () ...)barが, package FOOに存在するbarではなくて, cl-user::barを指すことになります.

これは, package CL-USERにおいてcl-user::barが, 他のbarという名前を持つsymbolをまるで隠してしまうように振る舞います. (メモ: 実際の専門用語としてshadowingなのかmaskingなのか分かってないので, あとで調べる.)

import

あるpackageに別のpackageに存在するsymbolをアクセス可能にする方法として, use-packageを紹介しましたが, これでは, external symbolが全てアクセス可能にされてしまいます.

特定のsymbolのみ, あるいはinternal symbolをアクセス可能にしたい場合には, importを使います.

例を考えます.

CL-USER> (defpackage :foo)
#<PACKAGE "FOO">
CL-USER> (in-package :foo)
#<COMMON-LISP:PACKAGE "FOO">
FOO> (cl:use-package :cl)
T
FOO> (defun hoge () 0)
HOGE
FOO> (defun piyo () 1)
PIYO
FOO> (defun fuga () 2)
FUGA
FOO> (export '(hoge piyo))
T
FOO> (in-package :cl-user)
#<PACKAGE "COMMON-LISP-USER">
CL-USER> 

package FOOを作って, そこにfoo:hoge, foo:piyo, foo::fugaが存在しています. foo:hoge, foo:piyoはexternalです.

use-packageを使うと, externalであるfoo:hogefoo:piyoもpackage CL-USERでアクセス可能になります.

foo:piyoだけアクセス可能にしたい場合は, importを使います.

CL-USER> (import 'foo:piyo)
T
CL-USER> (piyo)
1

current packageがCL-USERである時に, 'piyoだとCL-USER::PIYOを指すので注意してください.

externalではないsymbolも同様にアクセス可能にすることが出来ます.

CL-USER> (import 'foo::fuga) ; package FOOでfugaはinternalなのでコロンは2つ
T
CL-USER> (fuga)
2

また, importではアクセス可能になるsymbolはinheritedではなくinternされます.

shadowing-import

shadowはある名前のsymbolが指定するpackage(あるいはcurrent package)に存在することを保証しました.

あるpackageで別のpackageのsymbolがアクセス可能であることを保証したい場合もあります.

例えば, 2つのpackageのexternal symbolを全てアクセス可能にする(2つのpackageをuseする)ことを考えて, そのうちいくつかの名前が重複しているような場合にどちらか一方のsymbolのみを選択して, アクセス可能にしたい時等です.

どちらのpackageのexternal symbolも数個程度ならimportを使うことも出来ますが, 重複しているsymbolは数個なのに, external symbolは100個以上となると少々つらいです.

そのような時には, shadowing-importを用います.

先程のimportの例の続きを考えます.

CL-USER> (shadowing-import 'foo:hoge)
T
CL-USER> (hoge)
0
CL-USER> (defpackage :bar)
#<PACKAGE "BAR">
CL-USER> (in-package :bar)
#<COMMON-LISP:PACKAGE "BAR">
BAR> (cl:use-package :cl)
T
BAR> (defun hoge () 3)
HOGE
BAR> (export 'hoge)
T
BAR> (in-package :cl-user)
#<PACKAGE "COMMON-LISP-USER">
CL-USER> (use-package :bar)
T
CL-USER> (hoge)
0

foo:hogeshadowing-importします. この状態で, 別のpackage(ここではBAR)で同名のsymbolをexternalにして, そのpackageを使います.

HOGEという名前が重複するのですが, この状態ではpackage CL-USERでHOGEという名前はfoo:hogeを示すことが決まった状態になっているので, 衝突のエラーは出ずに, HOGEという名前でbar:hogeは参照できず, foo:hogeにアクセスできます.

shadowing-importでもimportと同様にsymbolはinternされます.

defpackage

そのうち書く.

謝辞

参考文献