Lambdaカクテル

京都在住Webエンジニアの日記です

Common LispでGPSを実装した + packageとpackage-inferred-systemまわりハマりどころ

初コーディングはCommon Lispでした。

さて、GPSというものがあります。地球を測位するやつではなく、General Problem Solverです。Generalという夢のような名前が付いていますが、このGPSは任意の形式化された記号問題を解くことができるらしいので遊んでみました。

ちなみにGPSのCommon Lisp実装はPAIPという本に載っています。日本では『実用 Common Lisp』という題で販売されています。

実用Common Lisp

実用Common Lisp

幸いにもPAIP本の中身はMITライセンスで公開されている(太っ腹!!)ので、これを見てコードを移植した。

github.com

なんでこんなことしているかというと、ちょうど先日Factorioの生産関連のデータを作成したので、そのデータを食わせて工場のプランニングとかができないかと試してみたいのだ。 そもそも工場のプランニングは記号問題なのか?という感じもするが、まあ試しに遊んでみたい。

github.com

ハマりどころ

でもって今回もちょっとしたハマりに遭遇したので、メモしておく。だいたいpackageまわりの挙動によるものである。

package-inferred-systemにおけるパッケージ同士の依存関係 - あるはずのシンボルが無い

今回はモジュールシステムにpackage-inferred-systemを採用した。これについては以下記事で説明しているので参照してほしい。

blog.3qe.us

さて、package-inferred-systemでは、パッケージ同士の依存関係を自動認識してモジュールを解決してくれる。 例えばfoobarというpackage-inferred-systemでfoobar/aというパッケージがfoobar/bというパッケージをuseしていた場合、これが依存関係と捉えられ、foobar/bfoobar/aに先立ってコンパイルされるようになる。

;; a.lisp
(in-pacakge :cl-user)
(defpackage :foobar/a (:use :cl :foobar/b))
(in-package :foobar/a)

(func) ; => "func"
;; b.lisp
(in-pacakge :cl-user)
(defpackage :foobar/b (:use :cl) (:export :func))
(in-package :foobar/b)

(defun func () "func")

さて、外部のパッケージのシンボルを呼ぶ場合にはuseする以外にも、 以下の例のようにfoobar/b:funcとして呼ぶこともできる。こうしてパッケージ名を明に指定することをパッケージ名を修飾する、などという。

;; a.lisp - package-inferred-systemでは動作しない
(in-pacakge :cl-user)
(defpackage :foobar/a (:use :cl))
(in-package :foobar/a)

;; パッケージ名を修飾して呼び出している
(foobar/b:func) ; => "func"

package-inferred-systemはパッケージのuse関係を依存関係解決に使っている

しかしながら、package-inferred-systemを使う場合は勝手が異なってくる。 useを行わずにパッケージ名を修飾することでシンボルを参照しようとした場合、そのシンボルは呼べない、というか呼び出しに失敗するのである。 なぜなら、シンボルを呼び出そうとしても、シンボル(と、シンボルがあるはずのパッケージ)が存在しないからである。

どういうこと?もうちょっと深く考えてみよう。

まず、どこかのパッケージにあるシンボルを呼び出すためには、シンボルとそれを入れるパッケージが先に存在していなければならない。これは当然の感覚だ。

シンボルが存在するとはどういうことかというと、先に定義されている、ということだ。 特にCommon LispをASDFのようなビルドツールでコンパイルしつつ使うような場合は、ASDFによって該当のソースファイルがコンパイルされることで、事前にシンボルが定義されていなければならない、ということである。

ちなみに、Common Lisp上でのプログラムの論理的な構成単位はパッケージだが、ビルドツールであるASDF上でのプログラムの構成単位は、ソースファイルである(簡略化している)。 ソースファイルとパッケージとの関係は独立しており、必ずしも1対1で対応しているわけではない。

そこに規約を加えるのがpackage-inferred-systemである。ASDFがpackage-inferred-systemが有効なシステムをコンパイルするとき、パッケージ名がそのままソースファイル名に対応しているとみなし、 ソースファイルのコンパイル順序を、パッケージのuse関係をもとに決定するようになる。

このため、直接useしていないパッケージのあるファイルは決してコンパイルされないという挙動が発生する。するとシンボルが定義されないので、呼ぼうとすると失敗してしまう、という現象が現れるのだ。

useを使わない場合には、空のimport-fromをつけることでASDFに依存関係を認識させる、というテクニックが使える。これが無いと依存関係が切断されてしまい、正しく動作できない。

;; a.lisp
(in-pacakge :cl-user)
(defpackage :foobar/a (:use :cl) (:import-from :foobar/b))
(in-package :foobar/a)

;; パッケージ名を修飾して呼び出している
(foobar/b:func) ; => "func"

パッケージを移動するのを忘れる

パッケージを作成するにはdefpackageを使うが、非常によく忘れがちなのが、(in-package :cl-user)(in-package :定義したパッケージ)である。

(in-package :cl-user) ;; ここを忘れがち!!
(defpackage :foobar (:use :cl))
(in-package :foobar) ;; ここを忘れがち!!

前者は、カレントパッケージを:cl-userに動かすもので、後者はカレントパッケージを作成したパッケージに移動させるものだ。

どうしてこのようなことが必要なのだろう?

その答えは、Common Lispはもともとインタラクティブな言語である、ということを踏まえると分かりやすい。その昔Common Lispは、現在のスクリプティング言語のように、直接プログラムを打ち込んで対話的に動作させていた。 LISPは古い時代からあるので、大きな1つのファイルに複数のパッケージを記述する、というスタイルが多かったかもしれない。そんな中Common Lispでもソースコードを分割するのが一般的になり、これを補助する目的でビルドツールが出現した。

Common Lispと同じように同一ファイル内に複数のパッケージを用意できるPerlとの間にある差異は、パッケージ宣言を行っても自動的にカレントパッケージが移動するわけではない、という点である。 Common Lispはインタラクティブな言語なので、REPLを前提として設計されている。REPLを通じて自由にカレントパッケージを移動できるようにするため、パッケージ宣言とパッケージ移動とを区別しているのかもしれない。

package prefixを忘れる

package-inferred-systemでは、システム名をパッケージの頭に付けなければならない。これを忘れると当然呼び出せないので、忘れないようにする。