第11章「パッケージ」

概要

ANSI Common Lispの第11章「パッケージ」を説明します。

パッケージとは何か

ANSI Common Lispは25の主要な章で構成されますが、その中でも「システム」と呼ばれるものが3つあります。
  • 第7章: オブジェクト システム
  • 第9章: コンディション システム
  • 第11章: パッケージ システム
パッケージシステムは最も単純に言えば「名前空間の管理」ですが、細かく見ていくといくつかの特徴があるのが分かります。パッケージシステムの機能と特色を簡単にまとめると、以下の通りです。
  1. パッケージは名前空間を提供しますが、パッケージが管理するのはシンボルです。
  2. パッケージはそれ自体が第1級のオブジェクトです。
  3. パッケージシステムは動的な仕組みとして提供されています。シンボルが属する名前空間は実行しながら決定されます。
  4. パッケージシステムは名前空間を拡張するだけでなく、「継承」の機能によって名前空間を融合することができます。
  5. パッケージシステムは名前空間を拡張したり融合するだけでなく、名前の衝突が発生した時の回避方法も提供します。
ポイントは、パッケージがオブジェクトであるという点と、パッケージシステムが動的であるという点です。このページでは一般的によく使われているdefpackageマクロだけでなく、そのマクロのベースになっている様々なオペレータを紹介しながら、パッケージシステムの特徴を紹介したいと思います。

パッケージのスロット

パッケージはオブジェクトなので、いつくかのスロットを持っています。この節ではスロットにアクセスするための読み取り関数をまず紹介します。
Function: package-name
パッケージ指定子を引数に取り、その名前を返す関数です。
Function: package-nicknames
パッケージ指定子を引数に取り、そのニックネームのリストを返す関数です。
Function: package-shadowing-symbols
パッケージ指定子を引数に取り、そのshadowing symbolsのリストを返す関数です。shadowing symbolsは名前の衝突を回避するために用いられる仕組みで、このシンボルはそのパッケージ内で確実にアクセス可能であることが保証されます。
Function: package-use-list
パッケージ指定子を引数に取り、そのパッケージが継承している親パッケージの実体のリストを返す関数です。
Function: package-used-by-list
パッケージ指定子を引数に取り、そのパッケージを親パッケージとして継承しているパッケージのリストを返す関数です。
「パッケージ指定子」とは、パッケージを特定できる文字列、シンボル、インスタンスのいずれかです。

例えば、ANSI Common Lispでは標準でCOMMON-LISP-USERというパッケージが提供されており、処理系のREPLを立ち上げた時はこのパッケージが現在のパッケージになっています。以下のパッケージ指定子はいずれもこのCOMMON-LISP-USERパッケージを示します。
(package-name 'common-lisp-user)
; => "COMMON-LISP-USER"

(package-name "COMMON-LISP-USER")
; => "COMMON-LISP-USER"

(package-name *package*)
; => "COMMON-LISP-USER"

*package*
; => #<PACKAGE COMMON-LISP-USER>

一番下の*package*というスペシャル変数はANSI Common Lispの標準で定められている変数で、現在のパッケージの実体が束縛されています。

また、パッケージには「ニックネーム」という別名を付けることができます。一般にCommon Lispでは「長くなっても意味を適切に示すような名前」を付けることが慣例化しており、長い名前を付けることに抵抗はありません。しかし、パッケージはシンボルと組み合わせて利用されるため、パッケージ名が長いとシンボル全体がとても長くなってしまいます。そのため、パッケージには短いニックネームをつけることが一般的です。
(package-nicknames 'common-lisp-user)
; => ("CL-USER" "USER")

COMMON-LISP-USERパッケージにはCL-USERというニックネームとUSERというニックネームが登録されています。このうち前者はANSI Common Lisp標準仕様で定められているので、どの処理系でも同じだと思いますが、後者は処理系によって異なる場合があります。

ニックネームはそれ自体がパッケージ指示子として使えます。
(package-name 'cl-user)
; => "COMMON-LISP-USER"

5つのスロットのうち、package-namepackage-nicknamesはとてもシンプルな意味しかないのですが、残りの3つはパッケージの継承と名前の衝突回避というパッケージシステムの根幹に関わる重要なスロットです。そのスロットの意味は、これからの小節で徐々に説明することにして、次節では先に標準のパッケージについて説明します。

標準のパッケージ

ANSI Common Lispでは3つの標準パッケージが定められています。
Package: COMMON-LISP
ANSI Common Lispで定められた様々な変数や関数、マクロなど、標準のシンボルを全て管理しているパッケージです。ニックネームはCLです。
Package: COMMON-LISP-USER
処理系が起動した時に自動的に使われるパッケージです。パッケージを変更しなければ、ユーザーが定義した関数や変数などの使用したシンボルは全てこのパッケージに属します。ニックネームはCL-USERです。
Package: KEYWORD
特別な機能を持つパッケージで、「キーワード」を管理するのに使われます。このパッケージに属するシンボル(つまりキーワード)は自己評価値として扱われます。ニックネームはありませんが、その代わりパッケージ指定子を省略してコロン:だけでシンボルにアクセスすることができます。
パッケージのシンボルにアクセスするにはコロン:を使います。例えば、これまでにも使ってきた+という加算関数はcommon-lisp:+でもcl:+でもアクセスできます。
#'+
; => #<SYSTEM-FUNCTION +>

#'common-lisp:+
; => #<SYSTEM-FUNCTION +>

#'cl:+
; => #<SYSTEM-FUNCTION +>

キーワードも実は:でもkeyword:でもアクセスできます。ファイル操作でよく使う:if-existsというキーワードの例です。
:if-exists
; => :IF-EXISTS

keyword:if-exists
; => :IF-EXISTS

パッケージの継承

さて、前節において、COMMON-LISPパッケージでANSI Common Lisp標準の関数やマクロなどが管理されており、COMMON-LISP-USERパッケージが処理系起動時のパッケージであると説明しました。この時、2つのパッケージは当然ながら別物なのですが、処理系起動時には+関数は当然ながらcommon-lisp:+(つまりcl:+)関数のことを示しており、パッケージ名を指定しなくてもシンボルにアクセスすることができる状態になっています。

このように、あるパッケージに含まれるシンボルを、別のパッケージからアクセスする時、コロン:を使った完全なシンボル名でアクセスするのではなく、パッケージ名を省略してアクセスするような時に「継承」が用いられます。

あるパッケージがどのようなパッケージを継承しているかという情報は、package-use-list関数でパッケージオブジェクトのスロットを読み取ることで得ることができます。
(package-use-list 'cl-user)
; => (#<PACKAGE COMMON-LISP> #<PACKAGE EXT>)

これはGNU CLISPの実行例ですが、ANSI Common Lisp標準のCOMMON-LISPパッケージと、独自拡張のEXTパッケージを継承しているのが分かります。EXTパッケージは例えば終了コマンドのext:quitなどを提供しています。
(symbol-package 'quit)
; => #<PACKAGE EXT>

逆に、あるパッケージがどのようなパッケージに継承されているかという逆側の情報を得ることもできます。その情報はパッケージオブジェクトのpackage-used-by-list関数でスロットにアクセスすることで確認できます。
(package-used-by-list 'cl)
; => (#<PACKAGE CL-WHO>
;     #<PACKAGE MARKDOWN.CL>
;     #<PACKAGE SPLIT-SEQUENCE>
;     #<PACKAGE XMLS>
;     #<PACKAGE XMLS-SYSTEM>
;     #<PACKAGE CL-PPCRE>
;     #<PACKAGE CL-PPCRE-ASD>
;     #<PACKAGE ALEXANDRIA.0.DEV>
;     #<PACKAGE UIOP/COMMON-LISP>
;     #<PACKAGE UIOP/PACKAGE>
;     #<PACKAGE REGEXP>
;     #<PACKAGE POSIX>
;     #<PACKAGE EXPORTING>
;     #<PACKAGE SCREEN>
;     #<PACKAGE CLOS>
;     #<PACKAGE COMMON-LISP-USER>
;     #<PACKAGE EXT>
;     #<PACKAGE SYSTEM>)

ANSI Common Lisp標準のCOMMON-LISPパッケージはほぼ全てのパッケージに継承されることになります。もし継承しなければdefuncl:defunと書く必要があり、ifcl:ifと書く必要があり、nilcl:nilと書く必要があります。これではあまりにも不便なので、多くの場合は継承を利用してパッケージ名を省略したシンボルアクセスを使います。

パッケージの生成: make-package関数

では、簡単なサンプルでパッケージの継承を試してみましょう。

まず、シンボルを定義するためのパッケージを用意します。パッケージはオブジェクトなので、make-package関数でパッケージの実体を生成することができます。
(make-package 'datetime)
; => #<PACKAGE DATETIME>

make-package関数はパッケージ指定子からパッケージの実体を生成して返しますが、その実体はCommon Lispのパッケージシステム上に登録されるため、必ずしも返り値を変数に束縛する必要はありません。試しに、find-package関数でパッケージの実体を探してみます。
(find-package 'datetime)
; => #<PACKAGE DATETIME>

パッケージの移動: in-packageマクロ

パッケージを生成しただけでは、デフォルトのパッケージが変更される訳ではありません。作ったパッケージに移動するにはin-packageマクロを使います。デフォルトのパッケージは*package*スペシャル変数に束縛されているので、その変化を見てみます。
*package*
; => #<PACKAGE COMMON-LISP-USER>

(in-package "DATETIME")

*package*
; => #<PACKAGE DATETIME>

in-packageは関数ではなくマクロなので、引数が評価されてから処理される訳ではありません。'datetimeでも通る処理系もあるかと思いますが、GNU CLISPではエラーになりますので、文字列で指定します。

*package*の変化を見ればCOMMON-LISP-USERパッケージからDATETIMEパッケージに移動したことが分かりますので、このパッケージで新たなシンボルを定義します。
(defstruct date
  year
  month
  date)
; => DATE

(defparameter *today* (make-date :year 2017 :month 11 :date 19))
; => *TODAY*

*today*
; => #S(DATE :YEAR 2017 :MONTH 11 :DATE 19)

シンボルの外部公開: export関数

いくつかのシンボルがDATETIMEパッケージの管理下に入りました。そこで、特定のシンボルは他のパッケージからでもアクセスできるようにしたいと思います。

まずは、*today*だけを外部に公開します。シンボルを他のパッケージからでもアクセス可能にするための関数はexportです。
(export '*today*)
; => T

では、DATETIMEパッケージを抜けて、COMMON-LISP-USERパッケージに戻ります。
(in-package "CL-USER")
; => #<PACKAGE COMMON-LISP-USER>

外部公開されたシンボルである*today*は、そのパッケージ名と共に完全なシンボル名でアクセスすれば、定義されたパッケージ以外のパッケージからでも見ることができます。
datetime:*today*
; => #S(DATETIME::DATE :YEAR 2017 :MONTH 11 :DATE 19)

しかし、外部公開されていないシンボルは、他のパッケージからアクセスすることはできません。例えば、年を表すスロットにアクセスするためのdate-yearアクセッサ関数は外部公開されていないので、他のパッケージからは使うことができません。
(datetime:date-year datetime:*today*)
; #<PACKAGE DATETIME> has no external symbol with name "DATE-YEAR"

これは、オブジェクト指向の一部とされている「カプセル化」の機能をパッケージシステムが提供していることを示します。外部からアクセスするシンボルだけをexportするように心がけることで、不用意なアクセスを防ぐことができます。

パッケージの継承: use-package関数

DATETIMEパッケージの*today*シンボル(外部公開済)にアクセスすることができましたので、さらに進んで、このシンボルをパッケージ名なしでアクセスできるように、パッケージの継承をしてみます。

パッケージの継承はuse-package関数を使います。
(use-package 'datetime)
; => T
これで、*today*シンボルにそのままアクセスできます。
*today*
; => #S(DATETIME::DATE :YEAR 2017 :MONTH 11 :DATE 19)

(symbol-package '*today*)
; => #<PACKAGE DATETIME>

このように、use-package関数を使えば別のパッケージの外部公開されたシンボルを継承することができます。

このような継承関係はすでに述べたように、パッケージのインスタンスのスロットに保存されています。
(package-used-by-list 'datetime)
; => (#<PACKAGE COMMON-LISP-USER>)

名前の衝突と回避

前節で見てきたように、パッケージシステムの利用は以下の3ステップで行います。
  1. パッケージの生成(make-package)とシンボルの定義(defun, defparameter, defvar, defstructなど)
  2. シンボルの外部公開(export)
  3. パッケージの継承(use-package)
しかし、このステップの結果、問題が発生する場合があります。それが「名前の衝突」です。シンボルはパッケージ名と合わせて完全に特定できますが、パッケージの継承をしてシンボルだけでアクセスしようとした場合、複数のパッケージに同名のシンボルが含まれていると名前が競合してしまいます。これが「名前の衝突」です。

パッケージという仕組みは本質的に名前の衝突を回避するためのものです。パッケージを分けることで同名のシンボルが存在してもすぐには名前の衝突が発生せず、適切に分離することができるからです。しかし、「継承」を使ってパッケージ名を省略してアクセスしようとするときに、衝突が発生します。

パッケージオブジェクトのスロットであるpackage-shadowing-symbolsは、名前の衝突を回避する手段を提供します。この節では、パッケージシステムの肝である「シャドウ」について説明します。

衝突の実際

今回も前回と同じ例を使いますが、途中で名前の衝突が発生するシチュエーションにして試してみます。(話を簡便にするため、先ほどまでの処理系は一旦終了し、新しく立ち上げた処理系で試してみてください。)

まずはmake-package関数でDATETIMEパッケージを生成し、in-packageマクロで新しく作ったパッケージに移動します。
(make-package "DATETIME")
; => #<PACKAGE DATETIME>

(in-package "DATETIME")
; => #<PACKAGE DATETIME>

*package*
; => #<PACKAGE DATETIME>
今度もDATE構造体と*today*変数を準備しておきます。
(defstruct date
  year
  month
  date)
; => DATE

(defparameter *today* (make-date :year 2017 :month 11 :date 19))
; => *TODAY*

*today*
; => #S(DATE :YEAR 2017 :MONTH 11 :DATE 19)

そして、外部から使う可能性のあるシンボルをexport関数で公開しておきます。exportはシンボルのリストを引数に指定することで、複数のシンボルを同時に公開することができます。
(export '(date
          make-date
          date-year
          date-month
          date-date
          date-p
          copy-date
          *today*))
; => T

これで準備は整いましたので、COMMON-LISP-USERパッケージに戻ります。
(in-package "CL-USER")
; => #<PACKAGE COMMON-LISP-USER>

ここでuse-packageすれば前回と同じなのですが、ついうっかり継承をする前に*today*シンボルにアクセスしようとしたらどうなるでしょうか。
*today*
; *** - SYSTEM::READ-EVAL-PRINT: variable *TODAY* has no value
; The following restarts are available:
; USE-VALUE      :R1      Input a value to be used instead of *TODAY*.
; STORE-VALUE    :R2      Input a new value for *TODAY*.
; ABORT          :R3      Abort main loop

当然ながらまだアクセスできないのでエラーになります。ここではabort( Restart )を選択して復帰します。(コマンドは:r3です。)

一度間違えてしまいましたが、改めてuse-packageで継承をすれば正しくアクセスできるはずです。use-packageしてみましょう。
(use-package "DATETIME")
; *** - (USE-PACKAGE (#<PACKAGE DATETIME>) #<PACKAGE COMMON-LISP-USER>): 1 name
;      conflicts remain
;      Which symbol with name "*TODAY*" should be accessible in
;      #<PACKAGE COMMON-LISP-USER>?
; The following restarts are available:
; DATETIME       :R1      #<PACKAGE DATETIME>
; COMMON-LISP-USER :R2    #<PACKAGE COMMON-LISP-USER>
; ABORT          :R3      Abort main loop

これが「名前の衝突」の実際です。目の前でDATETIMEパッケージとCOMMON-LISP-USERパッケージの間で、*today*シンボルを巡る名前の衝突が発生したのです。

これはパッケージシステムを使い始めた頃によくある間違いです。*today*シンボルはDATETIMEパッケージに定義しましたが、CL-USERパッケージには定義していません。なぜだろう、と思うでしょうが、ポイントは一度間違えて入力している点にあります。

ANSI Common Lispではシンボルが変数や関数として定義されていることと、シンボルの実体がパッケージに属する状態になっていることは必ずしも一致しません。変数や関数はdefparameterdefunなどで明示的に定義しなければなりませんが、それはあくまでもそのように定義したシンボルのsymbol-valueセルやsymbol-functionセルに変数や関数の実体が束縛されるということを意味しており、シンボル自体はREPLに打ち込んだ時に生成されているのです。そして、そのように生成されるシンボルは標準では現在のパッケージである*package*に属することになります(シンボルのsymbol-packageセルが設定されます)。そのため、間違えて入力した*today*CL-USERに属する状態として準備されており、その上でuse-packageをしたため、DATETIMEパッケージの本物の*today*と衝突してしまったのです。

Restart による解決

ただし、名前の衝突は他のエラーに比べてばそれほど致命的なエラーとは言えません。なぜなら、解決方法は単純で、どのシンボルを優先するかを決めればいいだけだからです。
そのため、ANSI Common Lispでは名前の衝突が発生した際に、どのパッケージのシンボルかを適切に選択できるよう、コンディションシステムを使って回復手段の提示を行うことが求められています。前節の例でデバッガが提示したのがその回復手段( Restart )です。

今回であればCL-USERパッケージの方はミスによるシンボルであることが明白なので、DATETIMEパッケージの方を使います。先ほどの続きで:r1を選択すれば、名前の衝突はデバッガによって解決され、コンディションシステムの回復機能によって何事もなかったかのように処理が継続されます。
; *** - (USE-PACKAGE (#<PACKAGE DATETIME>) #<PACKAGE COMMON-LISP-USER>): 1 name
;      conflicts remain
;      Which symbol with name "*TODAY*" should be accessible in
;      #<PACKAGE COMMON-LISP-USER>?
; The following restarts are available:
; DATETIME       :R1      #<PACKAGE DATETIME>
; COMMON-LISP-USER :R2    #<PACKAGE COMMON-LISP-USER>
; ABORT          :R3      Abort main loop

:r1
; => T

*today*
; => #S(DATE :YEAR 2017 :MONTH 11 :DATE 19)

uninternによる解決

名前の解決はその都度デバッガによる Restart の選択で解決することもできますが、あまりスマートな方法とは言えません。むしろ、異常事態に直面してもなんとか処理を継続するためのもの、と捉えるべきです。

先ほどのようなミスによるシンボルの生成はunintern関数によってシンボルをパッケージから取り除くことで解決することができます。

入力しながら何度も試すのは大変なので、"datetime.lisp"というファイルを作成し、以下の内容を定義してください。
;;;; datetime.lisp

(make-package "DATETIME")
(in-package "DATETIME")

(defstruct date
  year
  month
  date)

(defparameter *today* (make-date :year 2017 :month 11 :date 19))

(export '(date
          make-date
          date-year
          date-month
          date-date
          date-p
          copy-date
          *today*))

(in-package "CL-USER")

そして、ファイルを作成したディレクトリで処理系を立ち上げ、loadします。
(load "datetime")
;; Loading file /Users/satoshi/Desktop/datetime.lisp ...
;; Loaded file /Users/satoshi/Desktop/datetime.lisp
; => T

今回も名前の衝突を発生させるために、*today*を入力してエラーを発生させておきます。
*today*
; *** - SYSTEM::READ-EVAL-PRINT: variable *TODAY* has no value
; The following restarts are available:
; USE-VALUE      :R1      Input a value to be used instead of *TODAY*.
; STORE-VALUE    :R2      Input a new value for *TODAY*.
; ABORT          :R3      Abort main loop

一旦abort(:r3コマンド)で戻ります。そして、unintern*today*シンボルを削除します。
:r3

(unintern '*today*)
; => T

すると今度は名前の衝突が発生しないので、use-packageで継承をしてもエラーが発生しません。
(use-package "DATETIME")
; => T

インタプリタ上でのリアルタイムな名前の衝突の解決にはuninternが便利ですが、この関数はシンボルをパッケージから完全に削除してしまうため、シンボルを残しておきたい場合には使うことができません。

この名前の衝突問題を最もスマートに解決してくれるのが、パッケージオブジェクトのpackage-shadowing-symbolsスロットを活用した「シャドウ」です。

shadowによる解決

名前の衝突は前述のデバッガによる回復手段の提示に見られるように、2種類のパッケージのどちらかを選ぶという方法で解決されます。
  1. 継承しようとしているパッケージのシンボルを優先する。
  2. 現在のパッケージに存在するシンボルを優先する。
このどちらかの方法で決定された「優先するシンボル」が「シャドウシンボル( shadowing symbol )」です。

まず、前節と同様に名前の衝突が発生するような状況を準備します。ここからはCL-USERパッケージの*today*シンボルにも値を束縛しておきましょう。

新たに処理系を立ち上げて、以下のように準備してください。
(load "datetime")
;; Loading file /Users/satoshi/Desktop/datetime.lisp ...
;; Loaded file /Users/satoshi/Desktop/datetime.lisp
; => T

(defparameter *today* "CL-USER's symbol!")
; => *TODAY*

これで*today*シンボルは名前の衝突が発生する手前まで準備ができました。

まずは「現在のパッケージに存在するシンボルを優先する」という2番を試してみます。あるパッケージにあるシンボルが存在することを確約するにはshadow関数を使います。
(shadow '*today*)
; => T

(use-package "DATETIME")
; => T

*today*
; => "CL-USER's symbol!"

*today*変数の評価結果を見ればわかる通り、DATETIMEパッケージの方は劣後し、CL-USERパッケージの現存するシンボルが優先しています。

このとき、package-shadowing-symbolsスロットも変化しています。
(package-shadowing-symbols "CL-USER")
; => (*TODAY*)

しかし、DATETIMEパッケージのシンボルも同時に存在しており、こちらはパッケージ名を指定すればアクセスすることができます。
datetime:*today*
; => #S(DATE :YEAR 2017 :MONTH 11 :DATE 19)

shadowing-importによる解決

同じように、今度は「継承しようとしているパッケージのシンボルを優先する」という1番の解決策を試してみます。処理系を再起動し、名前の衝突が起きる手前まで準備してください。
(load "datetime")
;; Loading file /Users/satoshi/Desktop/datetime.lisp ...
;; Loaded file /Users/satoshi/Desktop/datetime.lisp
; => T

(defparameter *today* "CL-USER's symbol!")
; => *TODAY*

継承しようとしている側のシンボルを優先する場合は、shadowing-import関数を使います。
(shadowing-import 'datetime:*today*)
; => T

こちらは名前の通り、シャドウをしながらインポートもしてくれるため、他のシンボルを使わない場合はこの段階でアクセスすることができます。
*today*
; => #S(DATETIME:DATE :YEAR 2017 :MONTH 11 :DATE 19)

もちろん、use-packageでパッケージを継承してもエラーになることはありません。
(use-package "DATETIME")
; => T

(date-year *today*)
; => 2017

shadowing-import関数は既存のシンボルがすでに存在する場合も、そのシンボルを残したまま別のシンボルを継承することはできません。それは名前の衝突が発生する状況そのものです。

そのため、この関数は既存の同名のシンボルが存在する場合はuninternしてからインポートする点に注意してください。つまり、こちらはuninternしてuse-packageするという解決策と同じことを意味します。

例えば、サードパーティ製のライブラリを使う場合、名前の衝突はいつ発生してもおかしくありません。自分が新たに定義しているパッケージにおいて複数のサードパーティ製ライブラリを使いつつ、そこで名前の衝突が発生している場合、どちらか片方を優先して利用することがあるでしょう。このような場合はshadowing-importが有効です。

defpackageマクロ

これまでに見てきたパッケージによる名前空間の分離と継承、そして名前の衝突の回避に関する動作は全て動的なシステムとして設計されていますが、実際のプログラミングではパッケージシステムを動的に扱う必要がある場合はそれほど多くありません。むしろ、プログラムの設計段階でシンボルの帰属については解決されるべきかもしれません。

そのため、パッケージに関する様々な操作を一度に行うことができるオペレータが用意されています。それがdefpackageマクロです。

Common Lispライブラリのデファクトスタンダード

現在公開されている様々なCommon Lispプログラムは多くがdefpackageを用いた統一的なパッケージ管理を行なっています。ここで説明するのはANSI Common Lispに定められた規則ではありませんが、多くのパッケージが従っている暗黙のルールです。

defpackageマクロの実際の使い方を説明する前に、そのようなルールを簡単に説明しておきます。
  1. defpackageマクロの記述は package.lisp または packages.lisp というファイル名のソースファイルに、単独で記述します。関数や変数の定義とパッケージの定義は明確に分離すべきです。
  2. 全てのファイルの先頭に、そのファイルのシンボルが帰属すべきパッケージを指定するため、in-packageマクロで現在のパッケージを指定します。多くの場合、package.lisp または packages.lisp ではCL-USERを指定し、他のソースファイルでは独自に定義したパッケージ名を指定します。
  3. defpackageマクロの(:export ...)オプションに記述する外部公開シンボルは、その意味や種類によって適切に分類して記載すべきです。ドキュメントが不足しているライブラリを使用する場合、この外部公開シンボルの情報が頼りになります。
  4. パッケージの名前やニックネームは重複が許されないため、有名なライブラリに同一のものが存在しないかどうか、よく確かめてから設定すべきです。短い名前で重複してしまうよりは、長い名前で重複しないほうが望ましいです。
  5. (:shadow ...)オプションを使うとANSI Common Lisp標準のCLパッケージに帰属するシンボルと同名のシンボルを使うこともできますが、それは推奨されません。処理系によっては警告を出す場合もあります。
  6. パッケージの名前やシンボルの名前は「大文字の文字列」で指定することもできますが、一般には「小文字のシンボル」を使うことが好まれます。ただし、quote(')を使った通常のシンボルではなく、:を使用したキーワードシンボルか#:を使用したどのパッケージにも属さないシンボルを使います。通常のシンボルを使った場合、本来定義したい独自パッケージではなくCL-USERパッケージに先に定義されてしまうため、パッケージを継承する際に名前の衝突が発生する可能性があります。
なお、ANSI Common Lispに定められたパッケージシステムと、サードパーティ製ライブラリであるAnother System Definition Facility(ASDF)は混同しがちですが別物です。パッケージシステムはシンボルを管理するためのもので、ASDFはモジュール(ライブラリ)を管理するためのものです。パッケージシステムは package.lisp などのファイルにdefpackageマクロを使って定義されますが、モジュールに関する情報は .asd という拡張子を持つASDF定義ファイルに記述します。ASDFはANSI Common Lispの標準ではありませんが、標準仕様のproviderequireが非推奨となっており、ASDFがデファクトスタンダード化していますので、こちらは第24章「システムの構築」で扱おうと思います。

オプションの評価順序

さて、以降では実際にdefpackageマクロの使い方を説明したいと思いますが、個々のオプションの意味は対応する関数としてこのページ内ですでに説明したものばかりですので、先にオプションの評価順序について説明しておきます。

defpackageマクロには以下のオプションが用意されています。(:sizeオプションは省略します。)
Option: (:nicknames ...)
パッケージのニックネームを定義します。ニックネームは複数列挙できます。make-package関数の:nicknamesキーワード引数に対応します。
Option: (:documentation ...)
パッケージのドキュメントを定義します。
Option: (:use ...)
パッケージが継承する親パッケージを指定します。親パッケージは複数列挙できます。make-package関数の:useキーワード引数に対応します。
Option: (:shadow ...)
パッケージのshadowing symbolsを定義します。シンボルは複数列挙できます。shadow関数に対応します。
Option: (:shadowing-import-from ... ...)
パッケージが継承すべき親パッケージの特定のシンボルをshadowing symbolsとして定義します。第1引数は親パッケージ名で、第2引数以降がシンボルです。シンボルは複数列挙できます。shadowing-import関数に対応します。
Option: (:import-from ... ...)
:shadowing-import-fromオプションと似ていますが、名前の衝突が発生する場合もシャドウを使わないので、エラーが通知されてデバッガが起動します。
Option: (:export ...)
パッケージの外部公開シンボルを定義します。シンボルは複数列挙できます。export関数に対応します。
Option: (:intern ...)
パッケージに帰属するシンボルを定義します。exportオプションと異なり、継承したシンボルを指定した場合は新しく定義し直されることはありません。シンボルは複数列挙できます。intern関数に対応します。
それぞれ対応する関数を示しましたが、defpackageマクロだけで相当な機能があることが分かると思います。そして、各オプションの評価順序は以下の通り定められています。
  1. :nicknamesはパッケージの生成と共に評価され、ニックネームが準備されます。
  2. :shadow:shadowing-import-fromが評価され、shadowing symbols が用意されます。
  3. :useが評価され、親パッケージのシンボルが継承されます。
  4. :import-from:internが評価され、その他のシンボルが継承または生成されます。
  5. :exportが評価され、外部公開シンボルがパッケージシステムに登録されます。
名前の衝突を起こさないためにシャドウに関するオプションが:use:import-fromよりも前に評価される点に注目してください。これは、このページで説明した動的なパッケージシステムを体験すれば納得できると思います。

また、:exportオプションはdefpackageマクロの最後に評価されますが、外部公開シンボルの定義よりも先に評価されることにも注意してください。defpackageマクロはCL-USERパッケージで評価するため、:exportオプションに通常のシンボルを設置すると、CL-USERパッケージにシンボルが生成されることになります。そのため、前小節のデファクトスタンダードの6番目で説明した通り、シンボルはキーワードか unintern symbol として列挙します。もちろんシンボルの名前を示す文字列を直接列挙しても構いませんが、その場合は大文字で書く必要があるため、一般には好まれません。

defpackageマクロの使用例

では、このページで示した簡単なDATETIMEパッケージをdefpackageマクロで実装する例を示します。

まず、パッケージ自体の情報はまとめて package.lisp ファイルに定義します。
;;;; package.lisp
(in-package :cl-user)
(defpackage :cl-simple-datetime
  (:nicknames :clsd)
  (:use :cl)
  (:export #:date
           #:make-date
           #:date-year
           #:date-month
           #:date-date
           #:date-p
           #:copy-date
           #:*today*))

そして、プログラムの本体は別のファイルに定義します。こちらのファイル名は、自由につけて構いません( package.lisp というファイル名も仕様上は何でもいいのですが、デファクトスタンダードになっているという意味で自由ではありません)。プログラムが大きくなると適切に分類してソースファイルを分割することが一般的です。
;;;; datetime.lisp
(in-package :clsd)

(defstruct date
  year
  month
  date)

(defparameter *today* (make-date :year 2017 :month 11 :date 19))

各ファイルにはloadなどを含めるべきではありません。ロードは第24章で紹介するASDFに任せるのが無難です。ここではまだASDFを使わないので、処理系のインタプリタで個別にloadします。
(load "package")
;; Loading file /Users/satoshi/Desktop/package.lisp ...
;; Loaded file /Users/satoshi/Desktop/package.lisp
; => T

(load "datetime")
;; Loading file /Users/satoshi/Desktop/datetime.lisp ...
;; Loaded file /Users/satoshi/Desktop/datetime.lisp
; => T

これでプログラムを使う準備ができました。後はパッケージを継承することもできます。
(use-package :clsd)
; => T

*today*
; => #S(DATE :YEAR 2017 :MONTH 11 :DATE 19)

名前の衝突はパッケージを定義する側ではなく、使う側が気を配るべき事項です。自分が継承しようとしているパッケージに衝突しうるシンボルがないかどうかを確認し、衝突の危険がある場合はshadow関数(またはdefpackageマクロの:shadowオプション)やshadowing-import関数(またはdefpackageマクロの:shadowing-import-fromオプション)で事前に shadowing symbol を準備して備えます。

その他のパッケージ関連オペレータ

これまでに紹介しなかったパッケージに関するオペレータをまとめて紹介します。
Function: list-all-packages
パッケージシステムに登録されているパッケージの実体のリストを返します。引数はありません。
Function: rename-package
パッケージ指定子の引数を2つ取り、第1引数のパッケージ名を第2引数のパッケージ名に変更します。第3引数にリストの形式で新しいニックネームを指定することもできます。
Function: import
shadowing-importと同じように特定のパッケージの特定のシンボルだけを継承しますが、シャドウの機能を使わずに単純に継承します。もし名前の衝突が発生すれば、パッケージの継承と同じようにエラーが通知されます。
Function: intern
文字列とパッケージ指定子を引数に取り、文字列を名前とするシンボルを特定のパッケージに帰属させます。もし既存のシンボルが存在すればそのシンボルを返し、存在しなければ新たに生成します。Common Lispのreaderの動作に組み込まれており、明示的に使わなくても暗黙的に使われています。返り値は多値になっており、多値の2番目はシンボルのステータスです。ステータスは4種類あり、新しくシンボルが生成されたことを示すnil、継承されたシンボルであることを示す:inherited、外部公開シンボルであることを示す:external、公開されてない内部専用のシンボルであることを示す:internalのいずれかです。以下で少しだけサンプルを示します。
Function: packagep
オブジェクトを引数に取り、パッケージの実体であるかどうかを示す述語関数です。
Macro: do-external-symbols
外部公開されたシンボルに対する繰り返し構文です。do-symbolsdo-all-symbolsという亜種もあります。以下でサンプルを示します。
以下は簡単なサンプルです。前節で定義したCL-SIMPLE-DATETIMEパッケージをloadした状態にしておいてください。
(load "package")
;; Loading file /Users/satoshi/Desktop/package.lisp ...
;; Loaded file /Users/satoshi/Desktop/package.lisp
; => T

(load "datetime")
;; Loading file /Users/satoshi/Desktop/datetime.lisp ...
;; Loaded file /Users/satoshi/Desktop/datetime.lisp
; => T

intern関数の使用例>
;; CL-SIMPLE-DATETIMEの外部公開シンボルを指定する
(intern "MAKE-DATE" :clsd)
; => CL-SIMPLE-DATETIME:MAKE-DATE ;
;    :EXTERNAL

;; CL-USERに存在する継承されたシンボルを指定する
(intern "INTERN" :cl-user)
; => INTERN ;
;    :INHERITED

;; 存在しないシンボルを指定したので、新たに生成される
(intern "FOO" *package*)
; => FOO ;
;    NIL

;; 単純に生成されたシンボルは内部専用の状態にある
;; 第2引数は任意で、省略すると現在のパッケージ(*package*)が使われる
(intern "FOO")
; => FOO ;
;    :INTERNAL

do-external-symbolsマクロの使用例>
(do-external-symbols (sym :clsd) (print sym))
; CL-SIMPLE-DATETIME:*TODAY* 
; CL-SIMPLE-DATETIME:DATE-MONTH 
; CL-SIMPLE-DATETIME:COPY-DATE 
; CL-SIMPLE-DATETIME:MAKE-DATE 
; CL-SIMPLE-DATETIME:DATE 
; CL-SIMPLE-DATETIME:DATE-YEAR 
; CL-SIMPLE-DATETIME:DATE-P 
; CL-SIMPLE-DATETIME:DATE-DATE 
; => NIL

0 件のコメント :

コメントを投稿