Prolog 写経記 その 88 if/1

(ほぼ) 毎日淡々と Prolog を写経します.元ネタはこちら.

Prologユーティリティライブラリ

Prologユーティリティライブラリ

今日からは「10 章 手続き的構造の導入」に突入です.
順番では case/1 になるのですが,その中で使われている if/1 から写経します.

解説

条件の定義.if P then Q はもし P が成功したならば Q を実行する.if P then Q else R は,もし P が成功したならば Q を実行し,そうでなければ R を実行する.

ふむ.
まるっきり Prolog らしからぬしろものですな.

モード

if(+)

ふむ.
っていうか,thenelse はいずこへ?

定義

では,こいつの定義を写経しませう.

:- op(900, fx, if).
:- op(850, xfx, then).
:- op(800, xfx, else).
if P then Q :-
	if_then_else(
		Q = (R else S),
		if_then_else(P, R, S),
	if_then(P, Q)).

if_then(P, Q) :-
	call(P), !,
	call(Q).
if_then(_, _).

if_then_else(P, Q, _) :-
	call(P), !,
	call(Q).
if_then_else(_, _, R) :-
	call(R).

うーみゅ... そう来たか...
っていうか... これは手強い...


まずはあまり見慣れているとは言えない最初の :- op(...) について.
これはオペレータ宣言といって,述語というかファンクタを単項あるいは二項演算子のように使うことを宣言するものです.
ファンクタというのは

f(a, b, c).

みたいな複合項の名前.この例では f
実は Prolog では + (単項も二項も) などの演算子はもちろん,普段何気なく使っている :- なんかもオペレータ宣言された述語に過ぎなかったりするようです.
つ・ま・り

A = B.

という記述,実は

=(A, B).

の簡略記法に過ぎないという次第.
んで,

:- op(900, fx, if).
:- op(850, xfx, then).
:- op(800, xfx, else).

ここでは三つのファンクタのオペレータ宣言をしています.第三引数が対象のファンクタ.
最初の引数は結合度,いわゆる優先度で,数値が小さい方が優先度が高いものになります.if が一番優先度が低くて,else が一番高いということです.
そして第 2 引数がキモ.
まず if の場合の fx ですが,これはファンクタ f が引数 x の前置単項演算子ということを意味します.xf だと後置単項演算子
引数を示すのが x だと同じ優先度の演算子を結合できないことを意味しているので,

if if P

とは書けないことになります.
ちなみに引数を y で表して fy とすると同じ優先度の演算子を並べることができる,右結合の前置単項演算子になります.例としては not/1

not not P

と書くと

not (not P)

のように扱われます.これは実際には

not(not(P)).

と書いたのと同じ.
そして xfx は非結合の二項演算子==< 等の比較演算子みたいな感じ.
ちなみに yfx だと Java なんかの代入演算子のような右結合の二項演算子xfy だと */ のような左結合の二項演算子になります.


ともあれ (JW),

if P then Q.

と書くと

if(then(P, Q)).

のように解釈されるとみた.
if よりも then の方が優先度が高いので,まずは

if (P then Q)

と解釈されて P then Q のところが then(P, Q) となり,if はこの項を引数として受け取る,と.たぶん.
同じく

if P then Q else R.

と書くと,

if(then(P, else(Q, R))).

になるとみた.
ちなみに,ここでの then とか else とかは関数のように評価されるわけではないことに注意.
最初に else/2 が評価されて,その後 then/2 が評価されて,そのさらに後に if/1 が評価される,というわけではありません.
ここではthen(P, else(Q, R)) という項を引数として if/1 が呼び出されるだけです.
たぶん.
そうか,だからこの述語は単に if/1 で,thenelse も出てこないのか.
なるほどなるほど.


そんなわけで (どんなわけで?),述語の定義へ.
まずは本体.

if P then Q :-
	if_then_else(
		Q = (R else S),
		if_then_else(P, R, S),
		if_then(P, Q)).

ぐはぁっ
って感じですが,述語を使う場面のみならず,定義でもオペレータ記法を使ってくれてますね.
これの頭部は

if(then(P, Q)) :-

と書いた場合と同じはずなので,唯一の引数が then というファンクタでアリティが 2 の複合項である場合にマッチします.たぶん.
節の本体は一見ややこしいですが,実は単純に if_then_else/3 を呼び出しているだけ.その引数がちょっとややこしいですが,具体化された変数は展開されたかのように項が組み立てられることがポイントかと.
例えば

if N == 0 then write('zero') else write('not zero').

なんて場合は,まず

if(then(N == 0, else(write('zero'), write('not zero')))).

となって,このとき if/1 の各変数は

P
N == 0
Q
else(write('zero'), write('not zero'))

となります.よって,if_then_else/3 に渡される引数はそれぞれ

  • else(write('zero'), write('not zero')) = else(R, S)
  • if_then_else(N == 0, R, S)
  • if_then(N == 0, else(write('zero'), write('not zero')))

となります.


その if_then_else/3 ですが,2つの節があります.まずは最初の節.

if_then_else(P, Q, _) :-
	call(P), !,
	call(Q).

まずは第 1 引数を評価します.上の例では

else(write('zero'), write('not zero')) = else(R, S)

ということになり,このマッチングは成功して RS がそれぞれ

R
write('zero')
S
write('not zero')

に具体化されます.
そこで第 2 引数を評価します.上の例では

if_then_else(N == 0, write('zero'), write('not zero'))

なので,ある意味再帰呼び出し.っていうか,間違いなく再帰呼び出し
そこでは N == 0 が評価されますが,これは元々の条件そのものです.
これが成功すれば第 2 引数が呼び出されますが,それは例では write('zero') ということで,意図したとおりです.
なんかすげー.
もし N == 0 が成立しなければバックトラックにより次の節

if_then_else(_, _, R) :-
	call(R).

が試されます.ここでは無条件に第 3 引数,つまり write('not zero') が評価されます.
かなりすげー.騙されてる気分...


ちょっと戻って,もし else を付けずに

if N == 0 then write('zero').

だったらどうなるか?
if/1if_then_else を呼び出すときの引数はそれぞれ,

  • write('zero') = else(R, S)
  • if_then_else(N == 0, R, S)
  • if_then(N == 0, write('zero'))

となります.
すると if_then_else/3call(P)write('zero')else(R, S) とマッチしないため失敗に終わり,バックトラックにより次の節へ移ります.
そこでは第 3 引数

if_then(N == 0, write('zero'))

が評価されます.
そこで if_then/2

if_then(P, Q) :-
	call(P), !,
	call(Q).
if_then(_, _).

最初の節で P つまり条件を評価して,成功すれば Q を評価します.
P を評価して失敗した場合はバックトラックして次の節へ移りますが,何もせずに成功します.
めっさすげー.


そんなわけで (どんなわけで?),確かに if P then Qif P then Q else R もうまく動くようです.
えらく凝ってるというか巧妙ですな.
if_then_else/3 に渡す引数なんて,高階関数の趣がありますね.
正直,今の自分ではこいつの動作を理解するのが精一杯,こんな述語を書くことはとうてい無理です.
論理プログラミングとか宣言的プログラミングとは全然関係ないと思うけれど,これはこれでかなりすごい.
PrologLisp なんかと似ていて,プログラムも単なるデータという感じなのですが,これはそれを有効に使っているような感じ.
オペレータ宣言と相まって,PrologDSL を考えるのも楽しいかも〜.

注意

if P then Q において,もし条件 P が失敗すれば何も実行されず,if は成功する.

ほいほい.

では使用例を写経しませう.

warning(X) :-
   if X < 0 then
       (write('Value is negative.'), nl).

これの使用例はないので適当に試すべし.

3 ?- warning(0).

Yes
4 ?- warning(1).

Yes
5 ?- warning(-1).
Value is negative.

Yes

ふむ.
述語の定義がすごかった割に,使うとつまんない...