package-inferred-systemに期待したもの

これまでのあらすじ

 package-inferred-systemは、pakageのinferred-systemである。

 なんだか体がだるい感じのぼくはこころに学びを沁みわたらせるため、ライブラリのパッケージをすっきりと書くことはできないか考えてみようと思った。Common Lispを書いていてディレクトリ構成とパッケージ構造を同じにしたいとき、以下の2点が面倒だ:

  • パッケージ名をわざわざパッケージ定義に書くこと
  • パッケージ名とディレクトリ名を一致させること
    • パッケージ構造を変えたとき、変更が漏れやすい

 そこでぼくは、1ファイル1パッケージのスタイルでプログラミングできるらしいpackage-inferred-systemを試してみようと考えたのだった──

TL;DR

 package-inferred-systemは、ディレクトリ構造からパッケージ名を決定するためのものではない。

package-inferred-system とは

 package-inferred-systemは、Common LispASDF拡張機能です。ASDFCommon Lispにおいてライブラリ定義・読み込みを司る、ほぼ標準のアドオンです。Pythonでいえば、setup.py(定義)とdistutilsに相当します。それにしてもPythonにおいてモジュールパッケージは別のものを指す言葉だったのかー*1、知らなかったぜ 😎 。「ほぼ標準」というのは、Common LispANSIで言語仕様が定められていますが、ASDFはその中には含まれておらず、しかしだいたいの処理系に始めからバンドルされているので準標準的である、という意味です。

 ASDFの公式ドキュメントに拠れば、package-inferred-systemとは

Starting with release 3.1.2, ASDF supports a one-package-per-file style of programming, whereby each file is its own system, and dependencies are deduced from the defpackage form (or its variant uiop:define-package).

ASDF Manual: The package-inferred-system extension

とのことです。この機能に対して、誤った期待をしていたために、いろいろもやもやさせられた、というのがこの記事の要旨です。

期待していたもの

 ぼくがone-package-per-fileという言葉で期待したのは、Pythonのモジュールシステムのようなものでした。つまり、次に述べるようなものです。

 Pythonでは、ソースコードのファイル自体がモジュールです。なので、以下のようなa.pyb.pyがあるディレクトリでインタプリタを立ち上げて

# a.py
s = 's in module a'
# b.py
s = 'string in module b'
>>> import a
>>> a.s
's in module a'
>>> import b
>>> b.s
'string in module b'

ということができるようになる、と思っていました。つまり、Common Lispで次のようなことができるようになるのだ、と。

;;;; a/hoge.lisp
(defun foo ()
  (format t "No defpackage!!~%"))
CL-USER> (use-package :a/hoge)
CL-USER> (a/hoge:foo)
"No defpackage!!

 いや、exportしてないじゃん、という意見はごもっともですが。exportは手でする必要はあるにしろ、ディレクトリ構造と一致したdefpackageを書かなくなるなら、それは楽だなあと思っていました。

実際のpackage-inferred-system

 じゃあ、実際にはどうだったか。

 こうでした:ファイルパスと一致したdefpackageをしておけば、ASDF側でディレクトリ構造からパッケージ名を想像して読み込んでくれる。

 つまり、冒頭に挙げた問題点

  • パッケージ名をわざわざパッケージ定義に書くこと
  • パッケージ名とディレクトリ名を一致させること

は解決してくれません!! 残念!!!

 ただ、便利な点がまったくないかというとそういうことでもありません。以前ならば.asdフィアルに書いていた依存関係を、package-inferred-systemが内挿してくれるために、書かなくてよくなります。たとえばpackage-inferred-systemでない以下のようなsystemがあったときに、

;; hoge.asd
(defsystem hoge
  :depends-on (:alexandria)
  :components ((:module "src"
                :components
                 (:file "fuga")
                 (:file "core")
                 (:file "hoge"
                  :depends-on ("fuga" "core"))))

以下のように書くことで、:components節がまるまる不要になります。

;; hoge.asd (package-inferred-system)
(defsystem foo
  :depends-on ("alexandria"
               "hoge/hoge"))

 ただし、各パッケージに該当するファイルには、それぞれ以下のようなdefpackageをする必要があります(長くなるのでin-packageは省略しました)。

;; hoge/hoge.lisp
(defpackage :hoge/hoge
  (:use :cl
        :hoge/src/core
        :hoge/src/fuga))
(defpackage :hoge/src/fuga
  (:use :cl))
;; foo/src/core.lisp
(defpackage :hoge/src/core
  (:use :cl))

 つまり、ディレクトリ構造と:useなどからパッケージの依存関係を推測してくれるんだよ…!!

な、なんだってー!?

おわりに

 package-inferred-systemはone-pakcage-one-fileのスタイルを支援する機能です。しかしそれは、パッケージ名を自動で内挿してくれる機能ではありませんディレクトリ構造とインポートの記述からパッケージ依存関係を推測してくれる機能です。

 この機能の恩恵を特に得やすいのは、パッケージ数(=ファイル数)が多く、ディレクトリ構造が複雑なプロジェクトです。そういったプロジェクトは.asdファイルとディレクトリ構造を手動で一致させる手間が増えるため、defpackageさえちゃんとしていれば、依存関係を書かなくていいpackage-inferred-systemはかなり有用でしょう。

 こうして、package-inferred-systemへの溜飲が下がり、これからは使ってみようかなあという気になったのであった。