日比野
啓
![]() |
Debian が Common Lisp のために用意している仕組みが何を目的としているのか、 なぜそのような仕組みが用意されているの かを理解するために、 まずは Common Lisp がどんな言語でライブラリをどのように構築しているのかを説明し ます。
Lisp のプログラムは、 S 式と呼ばれる、 トークンと入れ子の括弧の列で表現されます。 表記と構造を対応づけて説明するため に、 まずはドット . を使った表記から説明します。
S-exp : () | token | (S-exp . S-exp)
|
S 式は空の括弧 () かトークンか S 式のペア (ドットをはさんで括弧でくくったもの) ということになります。 S 式のペアの 構造を考えるには以下のようなポインタの組を考えると構造的な理解がしやすいです。
(A . B)
|
((A . ()) . (B . ()))
|
(A . (B . (C . ())))
|
このポインタのペアのことを Lisp では cons セルと呼びます。 左側のポインタは car、 右側のポインタは cdr と呼び ます。
cdr が cons セルあるいは空の括弧 () を指している場合は . と cdr 内の括弧 ( ) を省略できます
このように cdr で連なる連結リストを簡潔に表現することができます。 空の括弧 () は要素が一つもない空のリストというこ とです。 Common Lisp では空リスト () は nil と書くこともできます。 この . の省略まで含んだのが一般的に S 式と呼ばれて いる表記です。
Lisp の処理系はこのリスト構造で表現されるプログラムを処理することで実現できます。 LISP は LISt Processing language の略だというわけです。 LISP のプログラムがリスト構造と等価であるということが今回の話の重要なポイントなの で注意しておいてください。
では実際に Lisp のプログラムを見ていきましょう。 ここから先は実際に対話環境でプログラムを試しながら説明していきます。 CL-USER>というのが対話環境のプロンプトです。
関数の呼び出し ではもともと定義されている関数を呼び出してみます。 足し算を行なう関数+の例です。
CL-USER> (+ 1 2 3)
6 |
リストの最初の要素が関数の名前で、 残りの要素が引数です。 かけ算の関数*も使ってみましょう。
CL-USER> (* (+ 1 2) 3)
9 |
引数の計算を行なったのちに関数の呼び出しが行なわれます。 この引数の計算のことを引数を評価すると言い ます。
関数の定義 自分でも関数の定義を行なってみましょう。
CL-USER> (defun my-plus (x y)
(+ x y)) MY-PLUS CL-USER> (my-plus (* 2 3) 2) 8 |
2 つの引数を足し算する関数が定義できました。
(defun <関数名> (<引数>*) [<省略可能なドキュメント文字列>] <本体の式>*)
|
最後の body form の結果が関数の返り値になります。
特殊オペレーター - special operator Common Lisp の構文はほぼ S 式しかありません。 では条件分岐やループといったプログラムの制御構造はどうやって実現し ているのでしょうか。
たとえば条件分岐を行なうためにifという特殊オペレーターがあります。 tは真を示したいときに慣習的に使用する値 です。
(if <式> <条件が nil ではない> [<条件が nil>])
|
CL-USER> (if t (print "then") (print "else"))
"then" "then" CL-USER> (if nil (print "then") (print "else")) "else" "else" |
Common Lisp ではnilが偽でそれ以外は真です。 ifは関数で表現することはできません。 もし関数であったとす ると、
CL-USER> (defun my-if (p then else) (if p then else))
MY-IF CL-USER> (my-if T (print "then") (print "else")) "then" "else" "then" |
というように、 thenの部分もelseの部分も関数my-ifの引数ですから、 両方とも評価された後に my-if が呼び出されてし まうのです。
長くなりそうなので詳しくは述べませんが、 ループを実現できる機能としては C の goto のような動きをするgoという特殊 オペレーターがあります。
マクロの呼び出し Lisp では S 式で表現できる構文を自分でも定義することができます。 それが Lisp のマクロです。
まずはもともと定義されているマクロ and を使ってみます。
CL-USER> (and (print "A") (print "B") (print "C"))
"A" "B" "C" "C" CL-USER> (and (print "A") nil (print "C")) "A" NIL |
andマクロは引数の式の評価が真であるかぎりは残りを評価し、 偽 (nil) より後は評価しません。 最後に評価した値が結果 になります。 引数が無い場合はtが結果になります。 マクロは S 式を S 式に変換する機能だと考えるとわかりやすいです。 こ れはあるリスト構造を別のリスト構造に変換するということでもあります。 関数macroexpand-1を使うとandマクロでどの ような変換が行なわれたのかを見ることもできます。
CL-USER> (macroexpand-1 ’(and (print "A") nil (print "C")))
(IF (PRINT "A") (AND NIL (PRINT "C")) NIL) T |
引数の一つ目を条件とする if の式になりました。 評価はマクロが全て変換された後に行なわれます。 リストだと思って見て みれば以下のような変形です。
マクロの動きを理解するには S 式とリストを対応づけて見ていくのがコツです。
マクロの定義 自分でもandマクロのようなもの定義してみます。
CL-USER> (defmacro my-and (&rest forms)
(if forms (list ’if (car forms) (cons ’my-and (cdr forms))) t)) MY-AND CL-USER> (my-and (print "A") (print "B") (print "C")) "A" "B" "C" T CL-USER> (my-and (print "A") NIL (print "C")) "A" NIL CL-USER> (macroexpand-1 ’(my-and (print "A") nil (print "C"))) (IF (PRINT "A") (MY-AND NIL (PRINT "C"))) T |
carは cons セルから car を返す関数、 listは複数の引数をリストにして返す関数、 consは 2 つの引数を car cdr の順に指す cons セルを作る関数です。 &restは可変引数をリストで受けとるためのパラメータの指定 です。
このように、 マクロはリスト構造を変換するようなプログラムを書いて定義を行ないます。 マクロの定義の中身はマクロの展 開のときに評価が行なわれるということが、 ここでの重要なポイントです。
似たような動きをしているようですが、 オリジナルandとは少し違っています。 最後に評価されたものが結果にはなってい ないようです。 ここでは定義を単純にするために少し動きを変えてみました。
(defmacro <マクロの名前> (<引数>*) [<省略可能なドキュメント文字列>] <本体の式>*)
|
Common Lisp にも他の実用的な言語と同様に数多くのライブラリがあります。 他の言語とは異なるかもしれない事情はライブ ラリ内のマクロの存在です。 マクロの展開がすべて終わった後でないとプログラムをコンパイルし、 実行することができないか らです。
たとえば、 あるライブラリ A は別のライブラリ B のマクロを使用しているかもしれません。 すると、 A のコードは B のマク ロ定義をすべて展開した後でないとコンパイルすることができません。 さらに、 対話環境で開発を行なうことを考えたとき、 利 用することにしているライブラリをコンパイルが済んだ状態でロードしておきたいと思うかもしれません。 そのときにはライ ブラリをロードした後にマクロ展開を全て行ない、 その後にコンパイルする必要があるのです。 数多くのラ イブラリを使用することにしていたら対話環境を利用できる状態にするまでに多くの時間がかかってしまい ます。
このような状況を解決するために、 Lisp の処理系では、 マクロの展開とコンパイルが済んだ状態のイメージをダンプして保 存しておいて再利用するのが一般的です。
Common Lisp ライブラリのコンパイルを支援するためのライブラリとして ASDF があります。 Makefile のようなもので、 コ ンパイルに必要な情報を記述しておくことができます。 ASDF ではライブラリモジュールのことを system と呼んでいて、 system ごとに名前 (system name) を付けることになっています。 同じモジュール内でファイル間に依存関係があ る場合はそれを記述します。 コンパイルに必要な別の system がある場合はその system name も記述しま す。 Debian では cl-asdf パッケージです。 以下は SBCL のドキュメントにあった ASDF のシステム定義の例 です。
(defpackage hello-lisp-system
(:use :common-lisp :asdf)) (in-package :hello-lisp-system) (defsystem "hello-lisp" :description "hello-lisp: a sample Lisp system." :version "0.2" :author "Joe User <joe@example.com>" :licence "Public Domain" :components ((:file "packages") (:file "macros" :depends-on ("packages")) (:file "hello" :depends-on ("macros")))) |
hello-lispという名前のシステム定義を行なっています。
Common Lisp の処理系のダンプイメージを作りなおしてくれるツールです。 処理系のパッケージを追加するとダンプイメージ を作ってくれます。 ライブラリをインストールすると各々の処理系ごとにダンプイメージを作り直してくれま す。 ライブラリが ASDF に対応していることが条件です。 Debian では common-lisp-controller パッケージ です。
Common Lisp の処理系やライブラリの Debian パッケージ作成時に common-lisp-controller に対応させるための支援をして くれるツールです。
パッケージのビルドの過程で dh_lisp コマンド呼び出すようにすると、 パッケージ内の ASDF の定義を書いたファイル (.asd) を検索して、 common-lisp-controller を呼び出すフックをメンテナスクリプトに追加してくれます。 common-lisp-controller がメンテナスクリプトから呼びだされたときには asd ファイルの名前を見てダンプイメージ作り直し の対象かどうか調べてからダンプが行なわれます。 /etc/common-lisp/images/<implementation>に asd ファイルの名前を 書いておくと作り直しの対象になります。
現 状 だ と 例 え ば パ ッケ ー ジ イ ン ス ト ー ル 用 に は 以 下 の よ う な フ ック が 追 加 さ れ ま す 。
if [ "$1" = "configure" ] &&
which register-common-lisp-source > /dev/null; then register-common-lisp-source "#SYSTEMDIR#" fi |
#SYSTEMDIR#が asd ファイルの名前に置き換わります。
Common Lisp の処理系のパッケージを作成する場合には、 ダンプイメージ出力のスクリプトを用意して、 dh_lisp の引数に 与える名前に合わせた名前を付けてやれば、 やはり common-lisp-controller を呼び出すフックをメンテナスクリプトに追加し てくれます。
現状だと例えばパッケージインストール用には以下のようなフックが追加されます。
case "$1" in
configure) if [ -x /usr/lib/common-lisp/bin/"#IMPLEMENTATION#.sh" ] && which register-common-lisp-implementation > /dev/null; then register-common-lisp-implementation "#IMPLEMENTATION#" fi ;; abort-upgrade|abort-remove|abort-deconfigure) if which register-common-lisp-implementation > /dev/null; then unregister-common-lisp-implementation "#IMPLEMENTATION#" fi ;; esac |
#IMPLEMENTATION#が dh_lisp の引数に与える名前に置き換わります。
最後に今回の話で使用している Emacs 上の対話環境の紹介をしておきます。
SLIME は Emacs 用の Lisp 開発環境です。 Debian では slime というパッケージに入っています。 Emacs 側の Elisp で書かれ たクライアントと Lisp の処理系側で書かれたサーバーが通信しながら対話的な開発環境を実現しています。 Lisp の処理系側で 書かれたサーバーの実装は swank と呼ばれています。 swank を実装すれば他の Lisp の処理系でも slime を使うことができる らしいです。
Emacs からの利用方法ですが、 たとえば処理系に SBCL を使用する場合は.emacs には以下のように書いておけばよいで しょう。
(setq slime-auto-connect ’ask)
(setq inferior-lisp-program "sbcl") |
Emacs のキーバインドで、 対話環境で試験的に実行してみるときに良く使いそうなものを挙げておきます。
ANSI Common Lisp の仕様のオンラインドキュメントを SLIME から読むことができます。 Debian では hyperspec という パッケージがインストーラーのパッケージになっています。
たとえば w3m-el パッケージを入れておいた状態で .emacs で
(set-default ’browse-url-browser-function ’w3m-browse-url)
|
などとやっておくと emacs バッファ内で関数やマクロのヘルプを読むことができます。 次のキーバインドでドキュメント内 の検索を行なうことができます。
第
50 回東京エリア Debian 勉強会 2009 年 3 月
____________________________________________________________________________________________