TL; DR
#pred double(X, Y) :: '@(X) times two equals @(Y)'.
- ディレクティブ
#pred
は s(CASP) で使用される構文 - 述語の説明文を定義可能
-
#pred
という構文はSWI-Prologには定義されていない独自のもの - 実体は s(CASP) で定義された演算子であり、
term_expansion/2
を使用し説明文定義の述語に置換している
はじめに
Prologのソースコードを読んでいたところ、#pred
という構文を見つけました。
% 簡略な例を記載、実際の使用例はもう少し複雑だった
#pred double(X, Y) :: '@(X) times two equals @(Y)'.
メタプログラミングをしていることはイメージできるのですが、Prologにこのような構文はありません。調べてみると、 s(CASP)
というシステムで定義、利用されていることが分かりました。
本記事では、 #pred
の使用方法や、Prolog上でどのように評価、実行されているかの仕組みについて紹介します。
使用したバージョン
s(CASP)ではSWI-Prologを使用します。
$ swipl --version
SWI-Prolog version 9.3.21 for x86_64-linux
UbuntuでインストールしたSWI-Prologだとバージョンが合わず動かなかったので、以下のDockerfileを使用しています。
Gist: https://gist.github.com/Syuparn/b1797e497fcc55aa2103a473fe26a35d
s(CASP)について
s(CASP)は「Goal directed Constraint Answer Set Programming」の略で、論理プログラミングを行うためのシステムです。ASPという(Prologと別の)論理プログラミング言語1をProlog上で実装したもので、否定の演算子等ASP由来の機能でPrologを拡張しています 2。
公式説明とサンプルコード:
作者の論文:
#pred
ディレクティブの使い方
#pred
はこのs(CASP)の拡張に含まれる構文の1つで、述語に対して説明文を定義できます。
#pred double(X, Y) :: '@(X) times two equals @(Y)'.
実際に使ってみましょう。まずは、#pred
に続いて述語の定義を記載します。
?- pack_install(scasp).
:- use_module(library(scasp)).
% #predで述語の説明文を定義
#pred double(X, Y) :: '@(X) times two equals @(Y)'.
% 述語の定義
% Prologの演算子 `=` の代わりに s(CASP) の演算子 `#=` を使用している(動作は同じ)
double(X, Y) :- Y #= X * 2.
swipl
のインタープリターを開き、?
演算子(これもs(CASP)の構文です)を使うと説明文が表示されます。
% ソースコード読み込み
?- [pred_sample].
true.
% 説明文を表示
?- ? double(X, Y).
% s(CASP) model
{ double(X,Y)
},
{Y=2*X} .
もう少し読みやすい形式として、human_justification_tree/2
という表示用の述語も用意されています。
(実装は以下のサイトを参考にさせていただきました)
% ...
:- use_module(library(scasp/human)).
% ...
query_human_tree(Query, Human) :-
scasp(Query, [tree(Tree)]),
with_output_to(
string(Human),
human_justification_tree(Tree, [])).
?- query_human_tree(double(2, 4), Human).
Human = " 2 times two equals 4\n\n".
単に説明文を出すだけでなく、変数とパターンマッチした場合説明文側の変数もパターンマッチした値に置き換えられます。
?- query_human_tree(double(2, X), Human).
X = 4,
Human = " 2 times two equals 4\n\n".
どうやって動いている?
便利な機能ですが、Prolog本家に定義されてもいない構文がどうやって動いているのでしょうか?
演算子の定義
まずは、#pred
構文がどのような文法になっているかを確認します。
#pred double(X, Y) :: '@(X) times two equals @(Y)'.
double(X, Y)
は述語、 '@(X) times two equals @(Y)'
はアトムなので、独自要素は #pred
と ::
です。
これらは、s(CASP)で演算子として定義されていました。
(#pred
は、前置の単項演算子 #
と pred
をスペースなしで繋げて書いています)
:- module(scasp,
[
% ...
op(950, xfx, ::),
op(1200, fx, #),
op(1150, fx, pred),
]).
term_expansion/2
による構文の置換(マクロ)
続いて、上記構文がどのように実際の処理に置換されるかを見ていきます。
#pred
の構文は、述語 term_expansion/2
によって定義されていました。term_expansionはProlog組み込みの述語で、マクロ定義のようなことができます。
term_expansionについては以下の記事が分かりやすかったです。
s(CASP)では、以下のように #pred
を pr_pred_predicate/4
に置き換える定義がされています。
user:term_expansion((# pred(SpecIn)),
pr_pred_predicate(Atom, Children, Cond, Human)) :-
process_pr_pred(SpecIn, Atom, Children, Cond, Human).
pr_pred_predicate
の引数には、右辺の process_pr_pred/5
でパースした結果を渡しています。
process_pr_pred/5
のパース処理は以下のようになっています。
process_pr_pred(Spec::B, A, Children, Cond, Human) =>
atom_codes(B, Chars), % アトムを文字コードリストに変換
phrase(pr_pred(FmtChars, Args, Spec, Spec1), Chars), % 文字コードリストをパース
atom_codes(Fmt, FmtChars), % 整形した文字コードリストをアトムに戻す
revar(Spec1, Spec2, _), % 大文字始まりのアトム(本来変数として扱うもの)を変数に置換
atom_cond(Spec2, A, Children, Cond), % 述語のパース(`-` 等のs(CASP)オリジナルの構文も解釈される)
Human = format(Fmt,Args). % 表示可能な形式に整形
置換された述語 pr_pred_predicate/4
は、以下のように表示の際に使用されます。
human_expression(Tree, Children, Actions) :-
tree_atom_children(Tree, M, Atom, ChildrenIn),
current_predicate(M:pr_pred_predicate/4),
\+ predicate_property(M:pr_pred_predicate(_,_,_,_), imported_from(_)),
human_utterance(Atom, ChildrenIn, M, Children, Human),
( Human = format(Fmt, Args)
-> parse_fmt(Fmt, Args, Actions)
; Actions = Human % html(Terms)
).
置換を確かめてみる
私のように疑り深い読者は本当に置換されているかまだ半信半疑かもしれません。一つ実験をしてみましょう。
Prologでは、ソースコード上で同じ述語を分割して記載すると警告がでます3。#pred
を使用した行の直後でこの警告を起こしてみます。
% ...
dummy(1).
#pred double(X, Y) :: '@(X) times two equals @(Y)'.
dummy(2).
% ...
?- [pred_sample].
% Pack scasp is up-to-date
Warning: /work/pred_sample.pl:8:
Warning: Clauses of dummy/1 are not together in the source-file
Warning: Earlier definition at /work/pred_sample.pl:6
Warning: Current predicate: pr_pred_predicate/4
Warning: Use :- discontiguous dummy/1. to suppress this message
true.
警告が表示されました。2つ目の dummy/1
の直前で pr_pred_predicate/4
を処理していると書かれています。確かに #pred
は pr_pred_predicate/4
に置き換わっています。
ディレクティブを自作てみる
最後に、s(CASP)の実装を簡略化してオリジナルのディレクティブを作ってみましょう。
説明文を表示する #desc
というディレクティブを実装します。
シンプルな実装なので、#pred
にあった変数置換や構文のパース等は対応していません。
#desc foo(X) :: 'This predicate is foo'.
先ほどのおさらいですが、必要になるのは
- 演算子定義
-
term_expansion/2
による置換
の2点です。
:- op(1200, fx, #).
:- op(1150, fx, desc).
:- op(950, xfx, ::).
term_expansion((# desc A :: B), description(A, B)).
#desc foo(X) :: 'This predicate is foo'.
動かしてみます。ディレクティブは description/1
の述語に置換され、想定通り説明文が取得できました。
?- [my_directive].
?- description(foo(1), Desc).
Desc = 'This predicate is foo'.
おわりに
以上、#pred
の構文の使用例と動作原理についての紹介でした。Prologのメタプログラミングが思ったよりも強力で驚きました。
s(CASP)にはまだまだ色々な独自構文、機能があるので、勉強したらまた記事にするかもしれません。
-
ASP.NETとは関係ありません ↩
-
厳密にはこれは s(CASP) の前身 s(ASP) の特徴で、s(ASP)に制約(Constraints) の機能を加えたものがs(CASP)のようです( https://arxiv.org/pdf/1804.11162 )。 ↩
-
一か所にまとめて定義した方が可読性が上がるからだと思われます。(Prologに詳しくないので間違っていたらすみません) ↩