初コーディングはCommon Lispでした。
さて、GPSというものがあります。地球を測位するやつではなく、General Problem Solverです。Generalという夢のような名前が付いていますが、このGPSは任意の形式化された記号問題を解くことができるらしいので遊んでみました。
ちなみにGPSのCommon Lisp実装はPAIPという本に載っています。日本では『実用 Common Lisp』という題で販売されています。
- 作者:ピーター・ノーヴィグ
- 出版社/メーカー: 翔泳社
- 発売日: 2015/06/02
- メディア: Kindle版
幸いにもPAIP本の中身はMITライセンスで公開されている(太っ腹!!)ので、これを見てコードを移植した。
なんでこんなことしているかというと、ちょうど先日Factorioの生産関連のデータを作成したので、そのデータを食わせて工場のプランニングとかができないかと試してみたいのだ。 そもそも工場のプランニングは記号問題なのか?という感じもするが、まあ試しに遊んでみたい。
ハマりどころ
でもって今回もちょっとしたハマりに遭遇したので、メモしておく。だいたいpackageまわりの挙動によるものである。
package-inferred-system
におけるパッケージ同士の依存関係 - あるはずのシンボルが無い
今回はモジュールシステムにpackage-inferred-system
を採用した。これについては以下記事で説明しているので参照してほしい。
さて、package-inferred-system
では、パッケージ同士の依存関係を自動認識してモジュールを解決してくれる。
例えばfoobar
というpackage-inferred-systemでfoobar/a
というパッケージがfoobar/b
というパッケージをuse
していた場合、これが依存関係と捉えられ、foobar/b
がfoobar/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
では、システム名をパッケージの頭に付けなければならない。これを忘れると当然呼び出せないので、忘れないようにする。