Skip to content

Instantly share code, notes, and snippets.

@privet-kitty
Last active December 8, 2018 08:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save privet-kitty/a9a520cbb09db7f3dbcc34f3ab1fc1b7 to your computer and use it in GitHub Desktop.
Save privet-kitty/a9a520cbb09db7f3dbcc34f3ab1fc1b7 to your computer and use it in GitHub Desktop.
foo.asd defines a new module component that gathers all .lisp files in the directory. foo-infer.asd extends package-inferred-system and auto-generates each `foo/bar/all'-type subsystem.

ASDFの拡張についてのメモ

ASDFではソースファイルを扱うためにいちいちコンポーネントとして登録する必要があり、ディレクトリ中のすべてのソースファイルをまとめて扱うという機能がない。これはpackage-inferred-systemを使った場合も同じで、ASDFが.asdファイルから特定のソースファイルに辿りつくためにはどこかでファイル名を指定する必要がある。この点について、ASDFの拡張を考える。

最初にpackage-inferred-systemを使わない場合(例はfoo.asd)を扱い、その後でpackage-inferred-systemの拡張(例はfoo-infer.asd)について検討する。

cl-source-module: ディレクトリ中のすべてのソースファイルを含むモジュール

このようなコンポーネントはmoduleのサブクラスとして簡単に書ける。ただ、foo.asdでの実装はやや単純すぎるか。実用するとしたら、初回のcomponent-childrenの呼び出しでchildrenスロットに結果を保持しておき、2回目以降は、既存のcl-source-fileについては新しいオブジェクトを作らないようにするべきだろう。

  • :recursive tオプションで、直下だけでなく再帰的にソースファイルを収集するというデザインもありかもしれない。
  • component-childrenはただのアクセサとして呼ばれるので、本来はO(1)であるべきだろう。2回目以降はディレクトリのタイムスタンプを調べて変化がなかったら何もしないようにするべきかもしれない。その場合は:recursive tは導入できない。
  • cl-source-module中のコンポーネントの順番は保証されないということで良さそう。そもそも順序が重要な状況ならそれをファイル名で指定するのは良くないだろうし。
  • :componentsに明示的に指定したファイルについては順番を先にする、みたいなルールは考えられる。ただ、「順番を後にする」が必要な場合もありそうだし、cl-source-moduleがそれらの面倒を見るべきなのかどうか疑わしい。必要なら以下のようにすれば良いだけだし、そちらのほうが明快だろう。
:serial t
:components ((:file "initial")
             (:cl-source-module "bar")
             (:file "final"))

asdf/contribの中にwild-moduleというものを発見した。これが答えなのかもしれない。

One-package-per-file-or-directory style?

package-inferred-systemが絡む場合は、デザインが自明でない。foo-infer.asdの例は、同じディレクトリのすべての.lispファイルを再エクスポートしたall.lispを、locate-systemのタイミングでソースリポジトリ内に作るという仕組みになっている。いちいちall.lispを書かなくてもfoo/bar/alluseできるという意図だ。これは使用可能ではあるが、妥協の産物だとも感じる。すぐに思いつく短所は以下の通り:

  • ASDFがソースリポジトリを変更することになる。
  • 特定の名前(allなど)を特別扱いしている。
  • 実装が良くない。*system-definition-search-functions*の使い方が変数の意図とは異なっている。

最初の問題点はアウトプットトランスレーションを使えば解決できるかもしれないが、根本的には、package-inferred-systemに類似の新しいシステムを定義する必要がありそうだと思った。

この際、1パッケージ1ファイルのスタイルを1パッケージ1ファイルまたはディレクトリに変更するというという案がまず思いつく。つまり、サブパッケージfoo/baruseされたとき、bar.lispが存在せずbar/が存在する場合は、bar/*.lispを再エクスポートしているパッケージとみなす、というルールが基本になる。これは素朴なアイデアだが、問題点が2つある。

  • ルールの明快さが損なわれる。foo/barがbar/を指しているかbar.lispを指しているかはファイル構造に依存することになる。
  • 十分に強力でない。そもそもこの仕組みは、bar/all.lispのような:use-reexportのためだけのファイルを作らなくてもよくするためのものだったはずだが、そのためにはもっと柔軟である必要がある。というのも、foo/bar/allのように各階層を再エクスポートするパッケージは、さらにその下のfoo/bar/baz/allも再エクスポートしたりするものだからだ。だからといって、デフォルトでbar/以下の全ての*.lispを再帰的にスキャンして再エクスポートすれば良いかというと、それも不適切な場合がある。(例えば内部関数をbar/internal/に分離している場合など)

最初の問題点については、末尾スラッシュで区別する、つまりfoo/barならbar.lispに対応しfoo/bar/ならbar/*.lispに対応するという回避策もあるか。ただ、末尾スラッシュのみで重要な識別をするのは人間に優しくないし、普通は「良くないデザイン」とされる気がする。

wild-package-inferred-system

いっそのこと、最初から***をワイルドカードとして導入したほうがすっきりするかもしれない。つまり、

  • パッケージfoo/bar/*はbar/*.lispを再エクスポートしているパッケージである。
  • パッケージfoo/bar/**/*はbar/**/*.lisp、つまりbar/以下のすべての.lispファイルを再エクスポートしているパッケージである。

UNIXスタイルと共通しているので意味が明らかだし、このルールには、パッケージfoo/A/BがA/B.lispに対応するという基本原則を崩さずに済むという利点がある。

(最初に思いついた時は、foo/bar/**foo/bar/**/*の省略として許すべきかと思ったが、一貫性の観点ではあまり良くないように見える。というのも、同様にfoo/barfoo/bar/*の省略として許されることにすると話が元に戻ってしまうからだ。)

ただ、自動生成されたfoo/bar/*などのパッケージのpackage-nicknamesをどうするかという問題は残っている。foo/bar/*のニックネームをデフォルトでfoo/barなどに決めてしまうとやはり同じ問題が生じるので、ニックネームを必要とするユーザーには基本的にはrename-package等で後からニックネームをつけてもらうことになりそう。もっとも、自動でニックネームをつける(foo/bar/*foo/bar)オプション自体はあってもよいかもしれない。

wild-package-inferred-systemとして実現した。

;; -*- coding: utf-8; mode: lisp -*-
;; This ASDF extension auto-generates all.lisp file for a given
;; `foo/.../all'-type system if it doesn't exist. all.lisp file
;; use-reexports all the cl-source-files in the same directory.
;; Acknowledge: https://twitter.com/windymelt/status/1064944712413368320
(defpackage #:foo-infer.asd
(:use #:cl #:asdf #:uiop))
(in-package #:foo-infer.asd)
(defparameter *reexporter-name* "all")
(defparameter *auto-generation-pragma* ";; AUTO-GENERATED BY ASDF")
(defun last-system-name (system-designator)
"foo/bar/baz -> baz"
(etypecase system-designator
(string (if-let (p (position #\/ system-designator :from-end t))
(subseq system-designator (1+ p))
system-designator))
(symbol (last-system-name (coerce-name system-designator)))
(component (last-system-name (coerce-name (component-system system-designator))))))
(defun but-last-system-name (system-designator)
"foo/bar/baz -> foo/bar"
(etypecase system-designator
(string (if-let (p (position #\/ system-designator :from-end t))
(subseq system-designator 0 p)
system-designator))
(symbol (but-last-system-name (coerce-name system-designator)))
(component (but-last-system-name (coerce-name (component-system system-designator))))))
(defun generate-reexporting-form (dir system)
"Generates the UIOP:DEFINE-PACKAGE form for reexporting."
(let ((system-but-last (but-last-system-name system)))
`(define-package ,(intern (standard-case-symbol-name system) :keyword)
(:nicknames ,(intern (standard-case-symbol-name system-but-last) :keyword))
(:use :cl)
(:use-reexport ,@(mapcar #'(lambda (path)
(intern
(string-upcase
(strcat system-but-last "/" (pathname-name path)))
:keyword))
(remove *reexporter-name*
(directory-files dir #P"*.lisp")
:key #'pathname-name
:test #'string=))))))
(defun sysdef-generate-reexporting-file (system)
"Is defined only for side-effect and always returns
NIL. Automatically generates all.lisp for each foo/bar/all
system. This function doesn't update an existing all.lisp, if its
first line is not `;; AUTO-GENERATED BY ASDF'."
(prog1 nil
(let ((primary (primary-system-name system)))
(unless (equal primary system)
(let ((top (find-system primary nil)))
(when (typep top 'package-inferred-system)
(if-let (dir (component-pathname top))
(let* ((sub (subseq system (1+ (length primary))))
(path (subpathname dir sub :type "lisp")))
(when (string= *reexporter-name* (last-system-name system))
(let ((reexporting-form (generate-reexporting-form
(pathname-directory-pathname path)
system)))
(when (or (not (probe-file* path))
(and (eql *auto-generation-pragma*
(read-file-line path :at 0))
(not (equalp reexporting-form
(read-file-form path :at 1)))))
(with-output-file (out path :if-exists :supersede)
(println *auto-generation-pragma* out)
(writeln reexporting-form :stream out)))))))))))))
(pushnew 'sysdef-generate-reexporting-file *system-definition-search-functions*)
;; Example system
(defsystem "foo-infer"
:license "Public domain"
:class :package-inferred-system
:depends-on ("foo-infer/interface" "foo-infer/bar/all" "foo-infer/baz/all"))
(register-system-packages "foo-infer/bar/all" '(:foo-infer/bar))
(register-system-packages "foo-infer/baz/all" '(:foo-infer/baz))
;; メモ
;;
;; モジュールの各階層にall.lispを作って(uiop:define-package
;; :foo/bar/all (:use-reexports ...))と定義する作業を自動化する拡張。
;;
;; 1. package-inferred-systemが絡むと、ASDFのオブジェクトモデルだけで
;; 書くのは難しそうだった。いちおうはエクスポートされている関数だけで
;; 実現できるようだが、副作用のみを目的とした
;; *system-definition-search-functions*の追加は開発者の意図から外れて
;; いるかもしれない。
;;
;; 2. foo-inferシステムが別のシステムに依存している場合、そのシステム
;; にもsysdef-generate-all.lispが適用されてしまうという問題があるが、
;; all.lispの最初のフォームが:auto-generated-by-asdfである場合にのみ更
;; 新するので、考えられるケースでは安全と思う。
;;
;; 3. all.lispをソースリポジトリに自動出力するのはたぶん良いスタイルで
;; はない。アウトプットトランスレーションでキャッシュのほうに出力でき
;; れば、そのほうが良いはず。
;;
;; 4. register-system-packagesを自動化するのも簡単だろうけれど、自動化
;; するのが良いことかどうかはわからない。
;; -*- coding: utf-8; mode: lisp -*-
;; Acknowledge: https://twitter.com/windymelt/status/1064944712413368320
(defpackage #:foo.asd
(:use #:cl #:asdf #:uiop))
(in-package #:foo.asd)
(defclass cl-source-module (module)
()
(:documentation "Auto-gathers and registers all .lisp files in the directory."))
(defmethod component-children ((parent cl-source-module))
(mapcar #'(lambda (path)
(make-instance 'cl-source-file
:parent parent
:name (pathname-name path)))
(directory-files (component-pathname parent) #P"*.lisp")))
;; Example system
(defsystem "foo"
:pathname "src"
:serial t
:components ((:file "package")
;; CL-SOURCE-MODULE auto-gathers foo/src/bar/*.lisp.
(:cl-source-module "bar")
(:module "baz"
:components ((:file "hello")))))
;; メモ
;;
;; cl-source-moduleはディレクトリ中のすべての.lispファイルを子コンポー
;; ネントとする。
;;
;; 1. .lispを決め打ちせず、default-component-classに対応するファイルを
;; 集めるモジュール(single-type-moduleとかcollection-moduleとか?)を
;; 定義したほうがきれいだし一般性がありそう。ただ、
;; module-default-component-classはエクスポートされていないので、安全
;; に書けないかもしれない。
;;
;; 2. (setf component-children)メソッドを定義していないことで問題が起
;; きる可能性はあるだろうか? いずれにせよsetfには意味がないので、ユー
;; ティリティ化するならエラーを出した方が良さそう。
;;
;; 3. :serial tは指定可能だが、directory-filesはリストを辞書式順序で返
;; すみたいな保証はしてなかったはずなので指定してもあまり意味がない気
;; がする。そういう仕様にしたければ返り値をソートする必要があるかも。
;;
;; 4. component-childrenが毎回新しいコンポーネントを作るのがやや気にな
;; るが、これを直すのは難しくないはず。
;; 汎用ファイルタイプのほうも作ってみたが、実際のところ、.lisp以外が必
;; 要になるケースがあるのかどうかはわからない。
(defclass collection-module (module)
()
(:documentation "Auto-gathers and registers all the files in the directory whose file type conforms to the DEFAULT-COMPONENT-CLASS of this module."))
(defun module-default-file-type (module)
(sb-mop:slot-definition-initform
(find 'type
(sb-mop:class-slots
(sb-mop:ensure-class
(or (asdf::module-default-component-class module)
asdf::*default-component-class*)))
:key #'sb-mop:slot-definition-name)))
(defmethod component-children ((parent collection-module))
(let ((file-type (module-default-file-type parent)))
(mapcar #'(lambda (path)
(make-instance 'cl-source-file
:parent parent
:name (pathname-name path)))
(directory-files (component-pathname parent)
(parse-namestring (strcat "*." file-type))))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment