第24章「システムの構築(ASDF)」

概要

ANSI Common Lispの第24章「システムの構築」を説明します。

ASDFの概要

ANSI Common Lispの第24章は「システムの構築」です。システムとはCommon Lispで構築されたプログラムやライブラリを意味しますが、実際に利用されているサードパーティ製ライブラリはほぼ全て「ASDF」というモジュール管理プログラムを介して管理されています。

Common Lispは極めて動的な性格を持っており、インタプリタで直接入力したり、loadで処理系の中に取り込まれると関数や変数が使えるようになります。パッケージによってシンボルの名前空間を拡張・継承する仕組みはありますが、それはソースファイルとは全く無関係です。Common Lispにとって対話的であることは本質であり、インタプリタを持っている、ということ以上に言語の根幹に動的な性質が組み込まれています。

このような特徴は便利といえば便利ですが、静的にライブラリをロードして使ったり、自分が書いたプログラムをライブラリとしてどこからでも呼び出せるようにしておいたりする場合もあります。そのように使うには、ライブラリのロードに関して一定の規則が必要です。

ASDFはサードパーティ製のツールですが、デファクトスタンダードになっているライブラリ管理ツールです。ASDFの規則に従ってライブラリを配置し、設計すれば、可搬性の高いプログラムになります。そしてASDFはサードパーティ製でありながら多くの処理系に含まれるため、ANSI Common Lispに標準を超えつつも、ASDFの規則に従った方がむしろ可搬性が高まる、という状況にあります。

最近ではQuicklispの登場でライブラリのダウンロードも簡単になりましたが、システムの管理自体はASDFを使用しています。このページではASDFの概要を説明します。

ASDFのロード

ASDFは多くの処理系に含まれており、含まれていない場合でもasdf.lispをダウンロードしてloadするだけで使うことができます。私は実用面では主にCCLを、このサイトの解説向けにはGNU CLISPを、確認用にSBCLをそれぞれ使っていますが、SBCLとCCLにはASDFが含まれているので以下のようにするだけでロードできます。
;; SBCL の場合
(require :asdf)
; => ("ASDF" "asdf" "UIOP" "uiop")

macOSのGNU CLISPにはASDFが含まれていないので、上記の asdf.lisp をダウンロードして、適当なディレクトリに保存します。ダウンロードしてきたら、一度コンパイルをして、ロードします。CLISPはASDFのロードが少し遅いので、以下のようにロードした状態でイメージを保存し、次からはそのイメージを直接ロードするようにすると早いです。

イメージを保存する関数は処理系によって全く異なるので、各自でマニュアルを調べてください。GNU CLISPではext:saveinitmem関数です。
;; GNU CLISP の場合
(compile-file "asdf")
;; Compiling file /Users/satoshi/lisp/asdf.lisp ...
;; Wrote file /Users/satoshi/lisp/asdf.fas
; 0 errors, 321 warnings
; => #P"/Users/satoshi/lisp/asdf.fas" ;
;    321 ;
;    321

(ext:saveinitmem "clisp-asdf.core")
;; Wrote the memory image into clisp-asdf.core (6,302,192 bytes)
; Bytes permanently allocated:            171,840
; Bytes currently in use:               6,122,976
; Bytes available until next GC:        1,528,014
; => 6122976 ;
;    1528014 ;
;    171840 ;
;    20 ;
;    30685856 ;
;    232253

.bashrc などに以下の記述を含め、シェルを再起動して clisp コマンドを打てば、ASDFロード済のCLISPが起動します。
alias clisp="clisp -M $HOME/bin/clisp-asdf.core"

GNU CLISPはC言語で書かれているため、イメージファイルの容量が少ないのが特徴です。私は色々とよく使うライブラリを盛り込んでロードして、イメージをダンプして、そのイメージをロードするようにして使っています。

ASDFのフォルダ・ファイル構造

ASDFを最もシンプルに使うには、以下の2つのルールを守ると簡単です。
  1. ホームディレクトリの common-lisp ディレクトリにライブラリを配置する
  2. プロジェクト名をライブラリディレクトリの名前と asd ファイルの名前に使う
例えば、プロジェクト名が my-project で、私のホームディレクトリが /Users/satoshi/ の場合、以下のような構造にファイル・ディレクトリを配置します。
  1. プロジェクトディレクトリ: /Users/satoshi/common-lisp/my-project/
  2. asdファイル: /Users/satoshi/common-lisp/my-project/my-project.asd
Quicklispを使うとプロジェクトディレクトリが common-lisp 直下に展開されないので注意してください。

asdファイルの書き方

ASDFは asd ファイルと呼ばれるスクリプトで管理しています。このスクリプトの中にはプロジェクトの著者やライセンス、構成物であるソースコードのリストと依存関係を記述します。

ここでは、最も広く利用されているサードパーティ製ライブラリであるAlexandriaalexandria.asd からドキュメントとテストを除いたものを引用して示します。
(defsystem "alexandria"
  :version "0.0.0"
  :licence "Public Domain / 0-clause MIT"
  :description "Alexandria is a collection of portable public domain utilities."
  :author "Nikodemus Siivola <nikodemus@sb-studio.net>, and others."
  :components
  ((:static-file "LICENCE")
   (:static-file "tests.lisp")
   (:file "package")
   (:file "definitions" :depends-on ("package"))
   (:file "binding" :depends-on ("package"))
   (:file "strings" :depends-on ("package"))
   (:file "conditions" :depends-on ("package"))
   (:file "io" :depends-on ("package" "macros" "lists" "types"))
   (:file "macros" :depends-on ("package" "strings" "symbols"))
   (:file "hash-tables" :depends-on ("package" "macros"))
   (:file "control-flow" :depends-on ("package" "definitions" "macros"))
   (:file "symbols" :depends-on ("package"))
   (:file "functions" :depends-on ("package" "symbols" "macros"))
   (:file "lists" :depends-on ("package" "functions"))
   (:file "types" :depends-on ("package" "symbols" "lists"))
   (:file "arrays" :depends-on ("package" "types"))
   (:file "sequences" :depends-on ("package" "lists" "types"))
   (:file "numbers" :depends-on ("package" "sequences"))
   (:file "features" :depends-on ("package" "control-flow"))))

おそらく、見れば意味が分かるでしょう。defsystemがASDFの主要なオペレータで、プロジェクトの構成を定義するのに用います。この関数は第1引数にプロジェクト名を取り、以下はキーワード引数です。重要なのは、:componentsキーワード引数です。この引数はリストであり、この中でファイル間の依存関係を示します。

パッケージの章でも示しましたが、defpackageマクロは package.lisppackages.lisp というファイルに単独で記載する慣例があるため、全てのファイルがパッケージファイルに依存することになります。その他のファイルは適宜必要に応じて記載します。

この asd ファイルにプロジェクトの依存関係を記載することで、依存関係の解決はASDFがやってくれることになります。

もう一つ、asd ファイルのパターンを紹介します。正規表現を行うCL-PPCREのASDF定義です。
(defsystem :cl-ppcre
  :version "2.0.11"
  :description "Perl-compatible regular expression library"
  :author "Dr. Edi Weitz"
  :license "BSD"
  :serial t
  :components ((:file "packages")
               (:file "specials")
               (:file "util")
               (:file "errors")
               (:file "charset")
               (:file "charmap")
               (:file "chartest")
               #-:use-acl-regexp2-engine
               (:file "lexer")
               #-:use-acl-regexp2-engine
               (:file "parser")
               #-:use-acl-regexp2-engine
               (:file "regex-class")
               #-:use-acl-regexp2-engine
               (:file "regex-class-util")
               #-:use-acl-regexp2-engine
               (:file "convert")
               #-:use-acl-regexp2-engine
               (:file "optimize")
               #-:use-acl-regexp2-engine
               (:file "closures")
               #-:use-acl-regexp2-engine
               (:file "repetition-closures")
               #-:use-acl-regexp2-engine
               (:file "scanner")
               (:file "api")))

先ほどのAlexandriaと異なる点が2点あります。情報を整理すると、以下のようになります。
  • defsystemの第1引数は文字列でもキーワードでも良い
  • 単純に上から読み込むことで依存関係を解決できる場合は:serialキーワード引数をtにする
AlexandriaもCL-PPCREも定番中の定番ライブラリですので、どのようなスタイルを好むかは完全に趣味の問題です。

プロジェクトのロード

common-lisp ディレクトリの中にプロジェクト毎のディレクトリが作成され、その中に適切な asd ファイルが配置されていれば、あとはプログラムのロードは一発です。
(asdf:load-system :alexandria)
; 0 errors, 0 warnings
; => T

(asdf:load-system :cl-ppcre)
; 0 errors, 0 warnings
; => T
asdf:load-system関数はプロジェクトの依存関係を解決して、コンパイルし、ロードしてくれます。コンパイルの結果は .cache などのキャッシュディレクトリに保存してくれるので、2回目以降のコンパイルは不要です。また、この関数は各環境のASDFディレクトリからロードするため、OSやASDFディレクトリの違いを吸収してくれます。

なお、処理系によってはANSI Common Lisp標準のrequire関数がこのasdf:load-system関数に置き換えられている場合もあるので、requireでもASDFのロードを行うことができる場合があります。

ライブラリ間の依存: :depends-on

ASDFの基本的な使い方の流れは以上ですが、実際のプログラムはファイル間の依存関係だけでなく、ライブラリ間の依存関係も解決しなければなりません。AlexandriaもCL-PPCREも共に単独でも用いることができるライブラリですが、例えば私がこのサイトのHTML生成に用いているmarkdown.clはいくつかのサードパーティ製ライブラリに依存しています。(註:現在はBloggerに移行したので、Markdownは使用していません。)
(asdf:defsystem markdown.cl
  :author "Andrew Danger Lyon <orthecreedence@gmail.com>"
  :license "MIT"
  :version "0.1.7"
  :description "A markdown parser for Common Lisp"
  :depends-on (#:cl-ppcre #:xmls #:split-sequence)
  :components
  ((:file "package")
   (:file "util" :depends-on ("package"))
   (:file "html" :depends-on ("util"))
   (:file "parser" :depends-on ("html"))))

:depends-onキーワード引数が使われているのが分かると思います。ここでは以下の3つのライブラリに依存しています。
  • CL-PPCRE
  • XMLS
  • split-sequence
ASDFはプロジェクト内のソースコードの依存関係だけでなく、ライブラリ間の依存関係も処理してくれます。ここでは、依存している3つのライブラリのロードを自動で行います。依存関係解決は再帰的に行われるので、もし依存ライブラリが何か別のライブラリに依存していれば、それが終わるまで再帰的にロードします。

ASDFのススメ

ASDFは標準のまま使うととてもシンプルでありながら、極めて便利な仕組みを提供してくれます。C言語に makefile があるように、Common Lispのシステム管理はASDFに任せるのがベストです。

一方、書き捨てのスクリプトなどはASDFを使用する必要はありません。そのようなものはdefpackageマクロも同一ファイルに書くか、全てのシンボルをCOMMON-LISP-USERパッケージに含めるなど、シンプルを徹底して利用することもできます。そのような場合は、compile-file関数でコンパイルしておき、load関数で1つのファイルをロードすればいいので、ASDFを使うよりも手軽です。私は、このサイトを作るのに使用しているCommon LispスクリプトはASDFを使ってライブラリのロードは行いつつも、そのスクリプトはASDFとしては定義しておらず、単一のファイルにまとめて書いています。

とはいえ、一般的にはできるだけASDFに対応してプロジェクトを記述すべきです。ASDFで定義されたものはどこからでもロードしやすく、プログラムの再利用性が高まります。他の言語と比べてもプロジェクトの管理は簡素で手軽だと思うので、ぜひ積極的に利用してください。

0 件のコメント :

コメントを投稿