Prologの述語に渡される引数が定義されているものなのかmeta_predicateを使って静的にチェックする
やりたいこと
他人に以下のようにPrologプログラムを書いてもらう時、
:- module(find_undefined, [foo/0]).
foo :- bar(buz).
述語bar
に渡すbaz
の定義が存在しない場合、
Emacs向けのSweepやVSCode向けのVSC-Prologなどの編集支援機構に警告してほしい。
不可能では?
Prologはかなり動的な言語なので、これは不可能な要求のようにも思えます。
しかし、
foo :- assertz(buz).
のようにassertz/1を使うと、
Undefined predicate buz/0, use C-c RET to define it
のように警告してくれます。
assertz
はビルドイン関数ですが、何かしらの方法で似たような属性を付けることが出来るのではないかと考えました。
調査
SWI-Prologのソースコードをassertz
で検索してみましたが、ビルドインライブラリの定義などにも使われているようなので数が多くて難しいです。
削除する方のretract
でも検索してみます。こちらの方が流石に使われている量は少ないと思うので。
ガチャガチャ検索してみた結果、 SWI-Prolog -- library(prolog_xref): Prolog cross-referencer data collection を経由して、 SWI-Prolog -- (meta_predicate)/1 に辿り着きました。 xrefよりはまだ言語機能に近い気がします。
SWI-Prolog以外で動くかは謎。一応GNU Prologには定義があるようですが… Predicate information
実装はどうせ自分でやるため、編集支援機能で正常に動けばそれで良いです。
meta_predicate
を使ってみます
警告を出す
:- module(find_undefined, [foo/0]).
:- meta_predicate bar(0).
bar(_).
foo :- bar(buz).
の場合、
Undefined predicate buz/0, use C-c RET to define it
と警告が出てくれます。
警告を消す
さて定義されたものの場合は警告されないようにします。
:- module(find_undefined, [foo/0]).
:- meta_predicate bar(0).
buz.
bar(_).
foo :- bar(buz).
のように雑に述語(事実?)を定義してやれば警告は消えます。
atomではなくなります
しかし、
bar(X) :- atom(X), write(X).
のようにatom
の場合write
するようにすると、この場合X
は述語のためfalse
を返すようになってしまいます。
またatomじゃなくなるのでパターンマッチにおいても、
:- module(find_undefined, [foo/0]).
% :- meta_predicate bar(0).
bar(buz).
foo :- bar(buz).
では、
?- foo().
true.
となりますが、コメントアウトを外して、
:- module(find_undefined, [foo/0]).
:- meta_predicate bar(0).
bar(buz).
foo :- bar(buz).
とすると、
?- foo().
false.
と動作が変わります。
そもそもatomは「定義」されるものではないので、仕方がないことではあるのですが。
パターンマッチは修飾すれば使えます
パターンマッチは行えないと不便ですね。
以下のように修飾して述語を参照するとパターンマッチも成功します。
:- module(find_undefined, [foo/0]).
:- meta_predicate bar(0).
buz.
bar(find_undefined:buz).
foo :- bar(buz).
?- foo().
true.
find_undefined:buz
という修飾名はめっちゃ長く思えますが、実際のプログラムではもっと短い名前を使います。
その短めの名前のモジュールの述語を参照すれば、集合の一つの要素のように扱えるため、下手なatomよりも堅牢なパターンマッチが出来るとも考えられます。
本当はもっと静的にチェックしていきたいのですけれど、その場合はそもそも動的なPrologを使うのが誤っています。 DSLを書くにしても自分でゼロから設計するのが正解ですね。