独学Common Lisp

シンボル、パッケージ、ASDF

SymbolとNamespace

これまで変数、関数、マクロなどを扱ってきましたが、それらは全て「シンボル」を通じてアクセスすることができます。例えば合計を計算する関数はsumというシンボルに、前回扱った自作のループマクロはforというシンボルに紐付けられており、我々はそのシンボルを通じて必要な機能にアクセスできます。

しかし、多くの関数・マクロを作ったり他の人が作成した関数・マクロを使用したりすると、シンボル名の衝突が発生することがあります。私もsumを定義しましたが、別の誰かが同じsumを定義しているかもしれません。このとき、sumだけではどちらか分からなくなってしまいます。これが名前の衝突です。

名前の衝突を避けるために、Common Lispは全てのシンボルを同じように扱うのではなく、いくつかのシンボルをまとめてグループ化する仕組みが用意されています。そのシンボルのグループ化の仕組みが「パッケージ(Package)」であり、「名前空間」を分けることができます。これは何も難しいものではなく、例えば住所と同じで、「中央区」だけでは数多くの政令指定都市で名前の衝突が発生しますが、「福岡市中央区」というように指定すればどこの中央区かを特定することができます。国内の住所は都道府県・市町村・区や地番などの階層に分かれており、最初から最後まで全く同じになる住所は存在しません。これも名前の衝突を避ける仕組みです。

Common Lispではパッケージとシンボルという二つの階層だけなのでJavaのように深く入り込みすぎることはありません。LISPはGNU Emacsのようなエディタが昔から充実しており、シンボル名の補完機能が付いていますから、階層を分けるのではなく長いシンボル名を使う傾向にあります。

このページでは実際のプログラム作成に向けて、パッケージを用いた名前空間の分け方とそのロードの仕方を説明します。Common Lispではパッケージの定義とロードが機能的に分かれているので柔軟に設計することができますが、実際にはASDFという事実上の標準があります。このMinimum Common LispではANSI Common Lispの標準を超える点を扱いませんが、このパッケージ管理システムだけはASDFの基本を扱うことにします。

symbol-packageと3つの標準パッケージ

これまでパッケージを意識したことはなかったと思いますが、実はCommon Lispはいかなる場合もパッケージを利用しています。全ての都市がどこかの都道府県に属しているように、全てのシンボルは何かのパッケージに属しています(例外として(gensym)で作成される衝突しないシンボルがあります。これは衝突することがないことが保証されており、読み取って使うこともありませんから、名前の衝突を避ける目的でパッケージに所属させる必要がありません)。

シンボルがどのパッケージに属しているかを調べるにはsymbol-packageを使用します。

(symbol-package '+)
; => #<Package "COMMON-LISP">

Common Lispの仕様標準に含まれる全ての機能はCOMMON-LISPというパッケージに属しています。

(symbol-package 'abc)
; => #<Package "COMMON-LISP-USER">

Common Lispの処理系を立ち上げた時は、COMMON-LISP-USERというパッケージからスタートします。自分で定義したシンボルはCOMMON-LISP-USERに属することになります。

(symbol-package :abc)
; => #<Package "KEYWORD">

:を付けたシンボルはKEYWORDというパッケージに属します。

この3つが標準で用意されているパッケージです。実際には処理系によってもっと多くのパッケージが用意されていますが、それらは仕様標準ではないので、他の処理系では使えないかもしれません。

(symbol-package 'quit)
; => #<Package "CCL">

Clozure CLでは終了するときにquitを使用しますが、これはCCLパッケージに属しており、標準の機能ではありません。

パッケージへのアクセス

シンボルが属するパッケージが分かっても、アクセスする方法が分からなければ意味がありません。パッケージ内部にアクセスするにはコロン:を使います。

(COMMON-LISP:+ 1 2)
; => 3
(COMMON-LISP-USER:+ 1 2)
; => ERROR

なお、ダブルコロン::でアクセスすることは推奨されません。必ずシングルコロン:を使用してください。

コロンはKEYWORDパッケージを参照するときも同じです。

(defvar a keyword:abc)
; => A
(defvar b :abc)
; => B
(equal a b)
; => T

ただし、パッケージ名を省略してコロンだけにすると、キーワードパッケージが参照されますから、keywordを付けることは実際にはありません。

ここで2つCommon Lispの特徴を説明しなければなりません。一つは、「Common Lispのパッケージとシンボルは大文字・小文字を区別しない」ということです。上の例では、defvar, b, keyword, abc, b, equalという6つのシンボル・パッケージが使われていますが、これらはどれも大文字で指定しても同じです。

もう一つの特徴は、キーワードパッケージのシンボルは事前に定義しておく必要がないということです。2つ上の例で+シンボルの名前空間をcommon-lisp-userで指定すると当然ながら定義されていないのでエラーになりますが、1つ上の例でabcというシンボルは初めてキーワードパッケージに導入されるにも関わらずエラーを起こしません。これは、キーワードパッケージのシンボルが本来的に「何もしない」からです。シンボルは通常、変数として使われて値を参照したり、関数として使われて計算処理を行ったり、マクロとして使用されてソースコードの改変に使用されたりします。キーワードはただのリテラル、つまり文字通りabc(ABC)と解釈されるために使うのです。もっとも、ほとんどはラムダリスト、つまり関数適用の際の&keyで使われ、他の場面ではあまり使いません。

パッケージの定義と使用

標準で3つのパッケージが用意されているとはいっても、全てをcommon-lisp-userパッケージに入れて使っていると全く意味がありません。パッケージを独自に定義するのが、defpackageです。

(defpackage :my-sample)
; => #<Package "MY-SAMPLE">

これだけで新しい名前空間MY-SAMPLEを得ることができます。

defpackageはパッケージを定義するだけで、そのパッケージに移動するわけではありません。つまり、depackageしただけではまだcommon-lisp-userにいます。これを変更して、新しいパッケージに入るのがin-packageです。

(in-package :my-saple)
; => #<Package "MY-SAMPLE">
(defvar a 1)
; => A

Clozure CLではこれだけで新しいパッケージで操作できますが、これは処理系による拡張です。試しにSteel Bank Common Lispで(defvar a 1)を入力するとエラーになります。なぜなら、my-sampleパッケージに移動したためにdefvarが定義されているcommon-lispパッケージが見えなくなったからです。そのため、defpackageを使う時は必ずcommon-lispパッケージを取り込むようにします。一度処理系を終了して、改めてパッケージを定義し直します。

(defpackage :my-sample
  (:use :common-lisp))

このように定義するとmy-sampleパッケージの中ではcommon-lispパッケージに定義されているシンボルをそのまま使うことができます。common-lispパッケージがないと何もできないので、必ず取り込むようにします。

ちなみに、一度my-sampleに移動すると、例えば前節で紹介したcclパッケージに定義されているquitが使えないので、Clozure CLでは処理系を正常に終了することができません。この時は、common-lisp-userに戻るか、cclに移動してからquitします。

(in-package :common-lisp-user)
; => #<Package "COMMON-LISP-USER">
(quit)

もし今どのパッケージに入っているのか分からなくなったら、大域変数である*package*にそのパッケージが入っていますから、確認することができます。

*package*
; => #<Package "COMMON-LISP-USER">

nicknamesとexport

depackageには色々なオプションがありますが、2つだけ覚えておかなければならないオプションがあります。

一つは:nicknamesオプションです。

(defpackage :my-sample
  (:use :common-lisp)
  (:nicknames :my))

パッケージにアクセスする際、コロン:を前につけてシンボルを参照しますが、いちいちパッケージ名ををフルで記述するのはとても面倒なので、省略することが許されています。その省略形を指定するのが:nicknamesです。

仕様標準のパッケージもニックネームが用意されており、common-lispclで、common-lisp-usercl-userでアクセスできます。keywordはすでに説明した通り、パッケージ名を指定せずにコロンだけでアクセスすることができます。

もう一つは:exportオプションです。

(defpackage :my-sample
  (:use :cl)
  (:nicknames :my)
  (:export
    #:sum
    #:for))

defpackageでパッケージを定義し、in-packageで移動し、そこで新たなシンボル(関数やマクロなど)を定義しても、外からアクセスできるようにしておかなければ使うことができません。一方、再帰を行う場合などの内部関数のように、直接外部から呼び出されることはないような関数はアクセスできるようにしておかなければいいのです。標準ではアクセスできませんから、アクセスできるようにするのが:exportオプションです。これで指定しておくと他のパッケージからコロン:だけでアクセスすることができます。

:exportでは#:を使ってシンボルを指定することが多いです。普通のコロンでも構いませんが、#:はどのパッケージに所属するわけでもないシンボルを示す特別なリーダーマクロです。exportする関数やマクロはkeywordパッケージに属するシンボルではなく、今から定義するパッケージに属するシンボルにしたいわけですが、この段階ではまだin-packageで移動することができないので、シャープコロンをつけてまだ独立したシンボルにしておき、実際にdefundefmacroで内容を定義するときにmy-sampleに属するようにするのです。

ダブルクォーテーションで囲って指定する場合もありますが、これで指定すると大文字と小文字が区別されるので、必ず大文字で記述する必要があります。

ASDF: パッケージの管理とロード

以上のdefpackagein-package、そしてコロン:によって名前空間の分離を行うことができます。しかし、プログラムが多くなってくると同一のパッケージ(名前空間)を利用しながらもソースファイルを分割したくなります。

また、一度作った便利な関数集・マクロ集などをまとめたユーティリティを用意しておき、様々なプログラムで使いまわしたくなるかもしれません。そのような場合にソースコードをコピーして新しいプログラムに持ってくるのは大変不便です。

そこで、ASDF(Another System Definition Facility)という仕組みが作られました。これはCommon Lispの標準仕様ではありませんが、Steel Bank Common Lisp(SBCL)から始まりほぼ全ての処理系に標準で実装されているソースコードの分割管理とライブラリの再利用化を促進する仕組みです。

ASDFの仕組みは主に2つの規則で実現されます。

  1. asdファイル: 分割したソースコードの依存関係を定義するファイル
  2. common-lispディレクトリ: 複数のライブラリを集中管理する場所

Common Lispはソースコードのコンパイルを行って高速に実行することができますが、コンパイルする際にはマクロの定義が必要になりますから、マクロ定義の読み込みはコンパイルの前に行わなければなりません。また、パッケージの定義も同じで、独自の関数を独自のパッケージに所属させるためには、事前にパッケージを定義しておかなければなりません。このような依存関係を一つにまとめて記述しておき、そのファイルを使ってコンパイルとロードを行う仕組みが1です。

1の手順で構築されたプログラムを再利用するには、別のプログラムから読み込める必要があります。そのためには、プログラムを保存する場所を事前に決めておき、その場所からプログラムを探すことが重要です。ASDFでは探すディレクトリを自分で変更することもできますが、標準で用意されているディレクトリがホームディレクトリの中のcommon-lispというディレクトリで、この中にパッケージ用サブディレクトリを作ることによってASDFが自動で読み込めるようになります。

Common Lispにはproviderequireという仕組みが用意されていますが、事実上誰も使っておらず、処理系によってrequireがASDF対応に拡張されているので、ソースコードを分割するようなプロジェクトでは必ずASDFを使用してください。

ディレクトリ構成

今回、参考として最も小さいASDFプロジェクトを作ってみます。まずはディレクトリ構成を整えましょう。

ホームディレクトリにcommon-lispというディレクトリが無ければまずは作ってください。そして、その中に今回作成するmy-sampleというパッケージのためのサブディレクトリを作成してください。

Linuxの場合は/home/<username>/common-lisp/my-sampleであり、macOSの場合は/Users/<username>/common-lisp/my-sampleになります。このディレクトリがパッケージ(プロジェクト)の保存場所になります。

asdファイルとdefsystem

このディレクトリにmy-sample.asdというファイルを作成してください。ファイル名はプロジェクト名と同じにして、拡張子はasdにします。

このファイルには、ソースコードとなるファイルの依存関係を記述します。書き方はとても単純です。

;;; my-sample.asd
(defsystem :my-sample
  :components
  ((:file "package")
   (:file "samples" :depends-on ("package"))))

ソースコードの分割の仕方には暗黙のルールがあり、パッケージの定義であるdefpackageはそれだけで分割してpackage.lispかpackages.lispというファイルで保存しておきます。通常、一つのプロジェクトでは一つのパッケージを使いますが、テスト用に別のパッケージを用意する場合があります。

この内容は見ての通りです。my-sampleというプロジェクトで、構成要素は2つのファイル(package.lispとsamples.lisp)です。samples.lispはpackage.lispに依存するので、先にpackage.lispをロードする必要があります。依存を示すのは:depends-onです。

ここでも2つの点に注意してください。一つは、拡張子がlispであれば補完されますから、通常は拡張子を付けないことです。もう一つは、ファイル名は必ずダブルクォーテーションで囲むことです。Common Lispは大文字と小文字を区別しませんが、ファイル名は区別するため、ファイル名を示す部分は必ずダブルクォーテーションで囲んで記述します。

あとはソースコードを記述して、ファイルに保存していきます。パッケージ定義であるpackage.lispは以下のようにします。

;;; package.lisp
(defpackage :my-sample
  (:nicknames :my)
  (:use :cl)
  (:export
   #:sum
   #:for))

プロジェクトの本体はどのように分割しても構いませんし、ファイル名も決まりはありません。依存関係を適切にasdファイルに記述しさえすればあとはASDFが処理してくれます。今回はsum関数とforマクロだけ定義してみます。

;;; samples.lisp
(in-package :my)

(defun sum (data)
  "Return the total amount of data.
  : list -> number"
  (apply #'+ data))

(defmacro for (var test change &body body)
  "Loop macro like C language 'for' sentence.
   EXAMPLE:
     (let ((sum 0))
       (for (i 0) (<= i 10) (incf i)
         (setf sum (+ sum i)))
       sum)"
  `(let (,var)
     (loop
        while ,test
        do
          ,@body
          ,change)))

ソースコードを記述する際も、2つの点に注意してください。

一つは、in-packageで作成しているパッケージに移ることを忘れないことです。これを忘れると定義が適切なパッケージに紐付けされません。

もう一つは、定義の説明を残すことです。Common Lispの関数とマクロは定義本体の先頭にダブルクォーテーションで括って説明を残すことができます。他の言語ではコメントで残すことが多いですが、GNU EmacsのSLIMEなどを利用すると定義の説明を後から読むことができますが、コメントではそのような機能を使うことができません。そのため、定義説明のスタイルの方が好まれます。

Perlなどでプログラムを組んだことがある人なら分かると思いますが、自分が書いたプログラムは半年も経てば内容を忘れてしまいます。なぜその関数やマクロを定義したのか、どのように使うのか、関数の返り値は何かなど、基本的な事項だけでも必ず説明を残すべきです。複数の人で共同作業するプロジェクトの場合はなおさらです。

asdf:load-system

ここまで終われば、あとは何も考えずに簡単にロードすることができます。自分が使っている処理系で、まずはASDFをロードしてください。

(require "asdf")
; => "asdf"
;    ("uiop" "UIOP" "asdf" "ASDF")

そして、ASDFを使って自分のプロジェクトをロードしてください。ロードはasdf:load-systemです。

(asdf:load-system :my-sample)
; => T

これでmy-sampleパッケージを利用できます。

(defvar *v* '())
; => *V*
(my:for (i 0) (<= i 10) (incf i)
  (setf *v* (cons i *v*)))
; => NIL
*v*
; => (10 9 8 7 6 5 4 3 2 1 0)
(my:sum *v*)
; => 55

ASDFはとても高機能なのですが、基本的な使い方はこれだけです。インターネットからダウンロードしてきたソースコードもほとんどがASDFの基盤の上に構築されているので、asdファイルが付いているはずです。その場合は適切なディレクトリ構成にするだけで自由に使うことができるようになります。

おまけ

追加で3つの機能と使い方を紹介します。

  1. 処理系の初期ロードファイル
  2. ソースコードの手動コンパイル
  3. 処理系依存の解決

ASDFはとても便利なので多くのシチュエーションで使用すると思いますが、言語の標準仕様には含まれていないので、毎回最初にASDF自体をロードしなければなりません。これは面倒なので、処理系が起動する際に読み込まれる初期化ファイルを利用してロードさせることが一般的です。

Clozure CLの場合は.ccl-init.lispまたはccl-init.lispが初期化ファイルであり、SBCLの場合は.bashrcが初期化ファイルです。もし初期化ファイルがなければ作成し、以下を記述します。

;;; ccl-init.lisp
(require "asdf")

これで処理系の起動時に自動的にASDFがロードされます。

CCLを使用している場合、起動が遅くなると思います。これはCCLに付属のASDFがコンパイルされていないからです。CCLに付属のASDFを直接コンパイルしておけば速くなります。

CCLのディレクトリはインストール方法によって違いますが、homebrewを使用した場合、/usr/local/Cellar/clozure-cl/1.11がそのディレクトリです。最後の数字はバージョンなので、適宜読み替えてください。

この中にlibexec/toolsというサブディレクトリがあり、その中にasdf.lispが入っていると思います。toolsまでターミナルで移動してからCCLを起動し、ソースコードのコンパイルを行うcompile-fileを実行してください。

(compile-file "asdf.lisp")
; => #P"/usr/local/Cellar/clozure-cl/1.11/libexec/tools/asdf.dx64fsl"
;    NIL
;    NIL

これでASDF自体を事前にコンパイルすることができますので、起動が早くなります。なお、compile-fileでソースコードをコンパイルする場合、マクロの定義とその使用が含まれる場合は一旦ソースコードをloadしてからコンパイルしてください。マクロはコンパイラへの指示ですから、事前にロードする必要があります。定義だけであればロードしなくてもコンパイルできます。

compile-fileは言語仕様に含まれるためどの処理系でも使用できますが、時には処理系に依存する機能を使うことがあります。例えば、ファイルをコピーする関数copy-fileはClozure CLやGNU CLISPには含まれていますが、SBCLには含まれていません。SBCLでは例えば最も有名な第三者パッケージであるAlexandriaに含まれるcopy-fileなどを使うことになるでしょう(自分で関数自体を定義してもいいかもしれません)。

このようなシチュエーションでは、CCLならccl:copy-fileを使用し、SBCLならalexandria:copy-fileを使用するという扱いになるでしょう。Common Lispには処理系によって読み込まれたり読み込まれなかったりするような、「使い分ける」機能が標準で規定されています。それが#+#-です。

例えば、copy-fileの場合、処理系依存をカバーする関数を一つ仲介させます。

(defun cp-file (from to)
  #+ccl (ccl:copy-file from to)
  #+sbcl (alexandria:copy-file from to))

#+がある場合、その処理系でだけ次の式が読み込まれます。逆に#-であれば指定された処理系以外の場合だけ読み込まれます。これでcp-file関数が処理系依存の処理を引き受けるので、他の部分では処理系依存を気にせず利用することができます。

メインで使う処理系は決める必要がありますが、エラーメッセージやデバッグのしやすさが違うので、案外他の処理系で動かしてみるということはあると思います。処理系依存の機能を隠蔽することができるのであれば、抽象化して隠蔽しておいた方が後で楽だと思います。

なお、ここで紹介したASDFの使い方は他にも様々なものがありますが、Alexandriaのasdファイルを参考にして紹介しました。Alexandriaはよく使われるサードパーティパッケージですから、一度ソースコードも参照してみてください。

参考文献

[ Example: 変数と関数 ]

(defvar list '(1 2 3))
(symbol-value 'list)
; => (1 2 3)
(symbol-function 'list)
; => #<Compiled-function LIST #x3000000BDAEF>

Copyright © 2017- satoshiweb.net All rights reserved.