LoginSignup
25
11

More than 3 years have passed since last update.

Common Lispでの開発実例 with package-inferred-system

Last updated at Posted at 2019-11-30

この記事は, Lisp Advent Calendar 2019の1日目の記事です.

Qiitaで私をフォローしてくださっている方も居るので, 昨年とは違って今年はQiitaに記事を書こうと思います.

この記事のライセンスはCC-BYとします.

また記事中のcldpfプロジェクトに含まれるコードはMIT Licenseでライセンス付けされています.

この記事の趣旨

2019年の秋頃から著者が書いているCommon Lisp製のポッドキャスト配信補助ツールcldpfを題材に, ASDFのpackage-inferred-systemを用いて、ライブラリ内のモジュールの依存関係を解決したプロジェクトの書き方について簡単に書く.

対象読者

この記事の対象読者は, Common Lispでの簡単な関数の使い方には触れたが, その後ライブラリの形にまとめての配布等にまだ手が出ていない方々.

取り扱うこと

  • defpackageを用いて, パッケージを定義して依存関係を明示すること
  • asdf, defsystem, package-inferred-systemを用いて, システムを定義して, システム内のパッケージの依存関係を解決すること

取り扱わないこと

  • Common Lispの開発環境構築
  • 具体的な開発フロー
  • 歴史的な事柄
  • cldpfが具体的にどういうことをするのか

環境構築に関しては, t-sinさんがLisp Advent Calendar 2017の記事として書かれた"いまから始めるCommon Lisp"などを参考にしてください.

具体的な開発フローを述べるところまでは, この記事でカバーすることは出来ないし, (誰もに共有できる)実践となるものは私が持っていないので, ここでは書かない.

歴史的事情は, 私が知らないので書かない. つまり, "私が学んだところ, 今こうなっているようだ"は書くが, 何故そうなっているのかは書かない.

cldpfはただの題材として扱う.

Common Lispにおけるモジュールの単位の簡単なおさらい

パッケージ

Common Lispでは, モジュールの単位としてpackageという概念が存在する.

packageは, 平たく言えば定義された関数や定数, グローバル変数などを名前空間を分けて(他のpackageのそれらから)分離することで, 管理する単位である.

同じpackageに取り込まれた(一例として, そのpackageの中で定義されると取り込まれる)関数名などは, 同じ名前空間に属する1.

それ以外にも, packageの仕組みは, あるpackage内で定義された関数名などを別のpackageで用いる方法等(インポート, エクスポート)も提供している. (packageは, defpackageを用いて定義される.)

これらはある名前空間内にどの様な名前があるのかを管理するシステムであり, プログラムのロードの仕組みとは別である.

また, 複数のpackageをひとまとまりにして取り扱う方法も提供されていない.

システム

Common Lisp自体の仕様ではないが, 多くの広く使われている処理系が取り入れているASDFというライブラリによって,
一つ以上のpackageをひとまとまりにして, systemという概念で運用している.

これらは, defsystemを用いて定義される.

この定義の中で, 他のsystemやファイル間との依存関係を指定することが出来て, asdfに用意されている命令で, systemに関連するファイルをロードすることが出来るようになる.

package-inferred-system

ASDFのバージョン3.1.2以降で, package-inferred-systemという仕組みを使えるようになった.

これは, packageとsystemそしてソースコードの書かれたファイルを1対1に対応させ, defpackage内の依存関係を解決しながら適切にロードすべきファイルを判断してロードするというような仕組みである.

それ以前では, defsystemを用いる際に, systemを構成するファイルや依存関係を書いていたが, package-inferred-systemを使うことを書くだけで,
適切にpackageとファイルを対応させておけば, その様な依存関係を明示する必要はなく便利である.

パッケージの定義

ここからは, cldpfを題材にして, defpackagedefsystemをどう書くかということを書く.

前提としてpackage-inferred-systemを使う.

cldpfの構成

cldpfのファイルディレクトリ構成から, 主となる部分を構成するCommon Lispのソースコードを抜粋すると以下の通りになる. (2019/11/10 時点)

.
├── cldpf.lisp
├── audio.lisp
├── feed.lisp
├── html.lisp
├── item.lisp
├── list.lisp
├── path.lisp
├── program.lisp
...

この内, cldpf.lispが, このソフトがユーザに提供するAPIについて書かれているメインのファイルとなる.

他のファイルは, このソフト(cldpf)を動かすために必要な機能をcldpf.lisp(あるいは他のファイル)に提供している.

例えば, audio.lispはmp3ファイルを適切な場所に配置するために, path.lispはcldpfが必要とする(ファイルやディレクトリの)パスに関する関数が定義されている.

(CLでの開発に限らず)多くのプロジェクト(ソフト)で同じことだと思うが, ある関連づいた機能や要素に関するソースコードをファイル毎に分けて管理することはままあり, ここでもその様な構成になっている.

これらの具体的な内容は置いておいて, ファイル先頭のdefpackageの部分だけ抜き出す.
また, defpackageと書いてきたが, ここではそれに変わるuiop/package:define-packageを用いている.
ここでの使用法だと, defpackageと大きく変わらない. (uiop/package:define-packagedefpackageに置き換えても良い.)

代表して, cldpf.lisp, path.lisp, program.lispuiop/package:define-package部分を列挙する. (以下のファイルの内容は, 2019/11/10 時点のものである.)

cldpf.lisp
(uiop/package:define-package :cldpf/cldpf (:nicknames) (:use :cldpf/path :cl)
                             (:shadow)
                             (:import-from :cldpf/audio :get-file-length
                              :copy-audio)
                             (:import-from :cldpf/path)
                             (:import-from :uiop/pathname
                              :ensure-directory-pathname :merge-pathnames*)
                             (:import-from :local-time :+rfc-1123-format+
                              :format-timestring :now)
                             (:import-from :cldpf/html :make-note-page
                              :make-index-page)
                             (:import-from :cldpf/item :item-list-template)
                             (:import-from :cldpf/feed :make-feed)
                             (:import-from :cldpf/program :make-program)
                             (:import-from :cldpf/list :write-items-list
                              :read-items-list :read-feed-list :write-feed-list
                              :read-item-list :read-program-list)
                             (:export :update-pages :update-item :make-item
                              :make-program :add-item)
                             (:intern))
path.lisp
(uiop/package:define-package :cldpf/path (:nicknames) (:use :cl) (:shadow)
                             (:import-from :uiop/pathname
                              :ensure-directory-pathname :merge-pathnames*)
                             (:export :get-feed-list-path
                              :get-items-list-path :get-program-list-path
                              :get-items-dir-path :get-item-list-path
                              :get-pages-dir-path :get-notes-dir-path
                              :get-audios-dir-path :get-index-file-path
                              :get-feed-file-path :get-note-file-path
                              :get-audio-file-path
                              :get-index-template-path
                              :get-note-template-path)
                             (:intern))
program.lisp
(uiop/package:define-package :cldpf/program (:nicknames) (:use :cldpf/path :cl) (:shadow)
                             (:import-from :uiop/filesystem :file-exists-p
                              :directory-exists-p)
                             (:import-from :uiop/pathname :merge-pathnames*
                              :ensure-directory-pathname)
                             (:export :make-program) (:intern))

いくつかの、 最低限の説明をする.

パッケージ名

uiop/package:define-packageの第一引数で, パッケージ名を指定する.

package名は, 前述のpackage-inferred-systemを使うのでファイル名に拠って決める.
今は, cldpf.asdという後述するシステムを定義するファイルと, すべて同階層に配置されているので,

cldpf.lisp, path.lisp, program.lispが, それぞれ

cldpf/cldpf, cldpf/path, cldpf/programとなる.

ソースコードをまとめてプロジェクトのディレクトリより一階層掘って配置することも多く見られるパターンかと思う.
その様な場合, 例えば, hogeという名前のシステムを定義するhoge.asd/home/user-name/hoge/hoge.asdという形で配置されている.
つまり, /home/user-name/hoge/がプロジェクトのディレクトリである時には,
package-inferred-systemの規約に沿った/home/user-name/hoge/src/piyo.lispで定義されるパッケージ名は, デフォルトではhoge/src/piyoとなる.

これを指定して別のパッケージ名にする方法もある. (システムの定義のところで説明する.)

外部への提供 :export

パッケージを作ることで名前空間を分けて, 機能毎に関数などをまとめることが出来るが, そのようにしてまとめられた関数はどこかで使われるものである.

そのため, パッケージで定義されている関数を他のパッケージで使えるようにする必要がある.

Common Lispでは, hoge::piyoの様にパッケージ名の後に:を2つ付けることで, そのパッケージに含まれるすべての変数や関数(シンボル)2にアクセスすることが出来るが,
それでは, どのシンボルをパッケージが提供しようとしているのかわからない.
つまりパッケージの作者が参照されることを意図していないものにまで参照することが出来てしまうので, この方法は推奨されているやり方とは言えない.

パッケージの作者は, パッケージを定義する際に:exportというキーワードを使って, 外に提供するシンボルを明示的に指定することが出来る.

例えば, 上で挙げたprogram.lispで定義されるcldpf/programというパッケージは, (:export :make-program)と書いてあるので, このパッケージの外側にmake-programを提供している.

そのため, cldpf/program:make-programとすることで, この関数を使うことが出来る.

また, use-packageなどで, そのパッケージに含まれるシンボルを別のパッケージに取り込むことが出来るが, この際にも:exportで指定したシンボルだけがパッケージ名のプリフィクスなしに使える.
(つまり別のパッケージに取り込まれる.)

他のパッケージでの定義を使う :import-from:use

今度は, 逆に別のパッケージから今から作ろうと(定義しようと)しているパッケージに取り込むシンボルの話である.

先程書いた, make-programは, cldpfの中で, ポッドキャストの一つの番組を管理するディレクトリを作る関数である.

cldpf/cldpfパッケージは, この関数を使ってユーザが管理するポッドキャストの番組を新たに追加する機能をユーザに提供する.

cldpf/cldpfの定義の中には, (:import-from :cldpf/program :make-program)という箇所があり, これによりcldpf/cldpf内では,
make-programとするだけで, cldpf/program:make-programを指すことになる.

:import-fromの第一引数はパッケージ名で, その後に取り込みたいシンボルが続く.
ここでは, 明示的に何というシンボルを取り込んだかということを書いておきたかったので, cldpf/programが提供するシンボルは一つだが,
:import-fromを使って, 明示的に取り込むシンボルを選択した.

例えば, cldpf/programの定義では, (:import-from :uiop/filesystem :file-exists-p :directory-exists-p)とあるが,
uiop/filesystemでは, delete-file-if-existsなどが提供されているが, それらはuiop/filesystem:delete-file-if-existsとしなければ,
cldpf/programの中で使えないのに対して, uiop/filesystem:file-exists-pは, file-exists-pだけで使える.

一方, cldpf/pathは, :useの中に書かれている. この様に(:use :hoge)とすると, パッケージhogeが提供する(エクスポートした)シンボルをすべて取り込む. 3

また, 逆にシンボルを取り込みたくは無いが, 依存関係だけ明示したい場合もある.
つまり, あるパッケージのエクスポートするシンボルを使う場合には明示的にhoge:piyoのように使いたいが,
これを呼び出す(評価する)際には, パッケージhogeがロードされていてほしいと言うような場合である.

この時には, :import-from (:hoge)の様に, 取り込むシンボルを書かずに, :import-fromを書けば良い.

システムの定義

さて, パッケージの定義についての後はシステムの定義である.

システムは.asdファイルで定義する. cldpf.asdより一部抜粋する.

cldpf.asd
(defsystem "cldpf"
  :depends-on("cldpf/cldpf")
  :class :package-inferred-system
  :author "myaosato"
  :licence "MIT"
  :mailto "tetu60u@yahoo.co.jp"
  :in-order-to ((test-op (test-op "cldpf/tests"))))

ここで, システムcldpfを定義している.

asdfがこのファイルを読める用にディレクトリに配置した後に, (asdf:load-system :cldpf)などとすることで, システムがロードできる. 4

package-inferred-system

package-inferred-systemを有効にするために, :class:package-inferred-systemを指定する.

これで前述したpackage-inferred-systemが有効になる.

パスの変更

パッケージの定義のところでも述べたように, ソースコードをsrc/などのディレクトリに配置した際に, パッケージ名に入るsrcが不要だと思うこともある.

もう一度例を書くと, hogeというシステム名のシステムを定義するhoge.asd/home/user-name/hoge/hoge.asdという形で配置されている.
つまり, /home/user-name/hoge/がプロジェクトのディレクトリである時には,
package-inferred-systemの規約に沿った/home/user-name/hoge/src/piyo.lispで定義されるパッケージ名は, デフォルトではhoge/src/piyoとなる.

この時に, :pathnameを使って, 以下のように書くことも出来る.

hoge.asd
(defsystem "hoge"
  :depends-on ("hoge/piyo")
  :class :package-inferred-system
  :pathname "src/"
  :author "hoge"
  :licence "MIT"
  :mailto "hoge@example.com")

この場合には, /home/user-name/hoge/src/piyo.lispで定義されるパッケージ名は, hoge/piyoとなる.

システムの依存関係

最後に依存関係とシステム内のpackageについて書く.

package-inferred-systemを使わない場合には, このシステムに含まれるファイルをcomponentsとして列挙していく方法が取られていた.

package-inferred-systemでは, ファイルとパッケージ, システムが一対一に対応しているので, システムに含まれるファイルもシステムの依存関係として記述する.
そのため:depends-on("cldpf/cldpf")とするだけで, このシステムをロードした時に, このシステム用意したファイルが読み込まれる.

cldpf/cldpfは, この様に明に指定するが, ここからパッケージの定義(:import-from:use)を追って,
このシステムcldpf/cldpf, つまりパッケージcldpf/cldpfであり, ファイルcldpf.lispが依存するパッケージ(システム, ファイル)の依存関係を解決してくれる.

これで, 関連する機能毎にファイルを分けて, パッケージとシステムを定義する最低限の方法についての説明は終わり.

書けなかったこと, 書かなかったこと

  • パッケージ定義の時の:nicknames, :shadow, :shadowing-import-from, :internなどの設定
  • テスト用のシステム(等関連するシステム)の定義の仕方
  • システム定義の時の:author, :licence, mailto, in-order-toなどの設定

最後に

意外と長くなってしまって疲れました.

尻すぼみになってしまった感じもしますし, 書けなかったことも多いので, 他にも記事を書きたいなと思っています.

そのためには, 私が知らないと行けない知識がもう少しありますが.

多くのプログラミング言語でもそうかと思いますが, 文法や標準ライブラリ内の簡単な関数を覚えて動くプログラムが書けるようになっても, それらを配布する形にまとめるまでに壁を感じる初学者が多いのでは無いかと思います.

ここで書いたことが, 自身のシステムの配布の方法がよく分からないために, コードの配布をためらっている人たちの後押しを出来たら嬉しく思います.

参考文献や関連資料


  1. 正確に書くなら関数と変数の名前空間も別れているが, ここではそのレベルの違いは気にしていない. また正確には関数が取り込まれるわけではなく関数名が取り込まれるので、関数と変数名の名前空間が別れていることとpackageシステムはレイヤーが違う. 

  2. 他の言語をつかっている方向けに変数と関数などというまどろっこしい書き方をしていたが, 面倒になったので以降シンボルと書く. Common Lispではパッケージがシンボルを含み. そのシンボルが値(関数も含めて)を参照する. はず, 詳しい人補足してください. 

  3. cldpf/pathは他に比べると多くのシンボルをexportしていて, いちいち書くのが面倒になった. この書き方の良し悪しはよく分からない...... 

  4. quicklispを使うなどの方法もある. 

25
11
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
11