[Prolog][Tips] 美しいPrologのコードの書き方(1)

Prologのコードを美しく書くにはどうしたらいいか?。少々ボリュームがあるので、根気のある人だけ読んでくださいw。でもきっとためになります。
まずPrologを書くに当たって、重要なことは「引数の性質が何であるか」と言うことです。
Prologの引数には3つの性質があります。

  1. 定数としての引数。これはhoge(+bar)とSWI-Prologのhelpに書かれています。
  2. 処理した後に値を返すための引数。hoge(-bar)。
  3. 上記どちらでも良い引数。hoge(?bar)。

たとえば、times(X,Y,Z)という述語を考えてみます。これは「X*Y=Z」という意味としましょう。このコードは以下のようになります。

times(X,Y,Z):-
	Z is X * Y.

あ、「`*’使ってたら意味ないじゃん」というつっこみは無しでw。例ですからw。このときもし

?- times(X,Y,3).

と実行したらどうなるでしょうか。

?- times(X,Y,3).
ERROR: is/2: Arguments are not sufficiently instantiated

はい、怒られました。つまり、「X、Y」は定数でなければならないと言うことです。理由は、かけて3になる二つの数は無限に存在するというのが数学的な理由ですが(有理数のかけ算を考えると分母、分子は無限にあることから分かると思います)、プログラミング的には、XとYをユニフィケーションしようとしてもユニフィケーションするためのデータがない、と言うことです。
このような述語の引数は

times(+X,+Y,?Z).

とHelpには書かれています。
では、そのほかの「-」や「?」で良いものとはどんなものでしょうか。これは先に言った「ユニフィケーションするために必要なデータが存在する変数」と言うことになります。たとえば以下のコードを考えましょう。

hoge(a).
hoge(b).

このとき

?- hoge(X).

と実行すると、

?- hoge(X).

X = a ;

X = b
?- 

となります。また、

?- hoge(a).

と実行すると、

?- hoge(a).
Yes
?-

となります。
このように、「hoge(X)」は定数でも、変数でも良いので

hoge(?X).

となります。
このようにPrologの引数には「定数でなければならない」ものと、「変数で良いもの」と「定数、変数どちらでも良いもの」の3種類が存在するわけです。

次に、この引数ににた構造として、Prologには値を保存するメモリ空間には主に2種類が存在します。

  1. Local変数。述語内部で使われる変数のメモリ空間。
  2. Globalメモリ。assert/1、retract/1で操作可能なメモリ空間。

なぜ、これが引数に関係するというかというと、「処理に必要な値をどこから取ってくるか」と言うことに関係してくるからです。

これら、「述語の引数の性質」、「アクセスするメモリ空間」を基点にして、述語を以下の3種類に分類します。そうすることで、美しいPrologのコード(構造化された)を書く事ができるでしょう。

  1. Procedure(手続き)
    • [意味] ある決まった手続きを順序通りに実行する。
    • [引数] +引数、-引数を複数か、引数無し。
    • [返値] 返しても返さなくても良い。
    • [名前] 引数の性質を表さず、手続きの内容を表す。
    • [空間] どの空間にもRead/Writeでアクセスして良い。
    • [終了] 必ずTrueで終了。
  2. Function(関数)
    • [意味] 引数を取って、値を返す。
    • [引数] 必ず+引数を取って、返値(-引数)を返すので、最低でも2個の引数をとる。
    • [返値] 必ず返す。
    • [名前] 写像の意味を表す名前にする。
    • [空間] Globalなメモリ空間には直接的にアクセスしない。つまり、処理をする前と後で、Globalメモリの状態は変わらない。
    • [終了] 必ずTrueで終了
  3. Predicate(述語)
    • [意味] 引数が何であるかを表す。
    • [引数] ?引数を複数取っても良い。
    • [返値] 基本返さない。つまり、「-」の引数を持たない。
    • [名前] 対象変数の関係、および性質を表す。
    • [空間] どの空間にも「Read」でアクセスして良い。
    • [終了] True/Falseで終了。

この三種類です。今自分が書いている述語ははたしてこの3つのどれなのかを意識しながら書くことで保守性が高く、綺麗なPrologのコードが書けます。では、おきまりの「家系図」みたいなプログラムを例に取って具体的なコードを上げてみたいと思います。なお、SWI-Prolog Ver 5.6.Xを仮定しています。SWI-Prolog Ver 5.6.XではUnicode対応なので、日本語も使えます。

まず元のデータは以下のものを使用します。NHK大河ドラマ風林火山」のページからw、武田家の部分だけ使用しました。

male(武田信虎).
male(武田晴信).
male(武田信繁).
male(武田義信).
female(大井夫人).
female(三条夫人).
female(萩乃).
female(由布姫).
parent(武田信虎,武田晴信).
parent(武田信虎,武田信繁).
parent(大井夫人,武田晴信).
parent(大井夫人,武田信繁).
parent(武田晴信,武田義信).
parent(三条夫人,武田義信).
married_couple(武田信虎,大井夫人).
married_couple(武田晴信,三条夫人).
married_couple(武田晴信,由布姫).
ancilla(三条夫人,萩乃).

ここで「それぞれのデータを、付け加えたり、消したりしたいな」という要望のため、おのおのの述語をDynamic指定しておきます。こうしないと、後で書き換えできなくなります。dynamicの使い方は、

:-dynamic [述語名]/[Arity(引数の数)]

と書きます。しかし、複数の述語をそれぞれdynamic指定するのは面倒です。そこで、Wrapperの述語を一つ付けましょう。そうすると以下のようにして簡単にデータをDynamicにできるとともに、扱いも良くなります。ここではそのWrapper述語を「data/1」としています。

:-dynamic data/1.
data(male(武田信虎)).
data(male(武田晴信)).
data(male(武田信繁)).
data(male(武田義信)).
data(female(大井夫人)).
data(female(三条夫人)).
data(female(萩乃)).
data(female(由布姫)).
data(parent(武田信虎,武田晴信)).
data(parent(武田信虎,武田信繁)).
data(parent(大井夫人,武田晴信)).
data(parent(大井夫人,武田信繁)).
data(parent(武田晴信,武田義信)).
data(parent(三条夫人,武田義信)).
data(married_couple(武田信虎,大井夫人)).
data(married_couple(武田晴信,三条夫人)).
data(married_couple(武田晴信,由布姫)).
data(ancilla(三条夫人,萩乃)).

こうしておけば

retractall(data(_)).

とするだけでデータを全て消すこともできますし、

retractall(data(male(_))).

と選択的に男だけ消すこともできます。ちょっとしたTipsです。

さて、次に「XはYの親である」という述語「parent/2」を作りたいと思います。ここで先ほどの「これはいったいどういう性質の述語か?」を考えるわけです。当然これは三番目の「Predicate」に分類されます。なぜならば、「X,Y」の関係(性質)を定義するからとともに「parent(?X,?Y)」という引数の性質を期待するからです。

そのPredicateは以下のように書けます。

parent(Parent,Child):- %直系の親子
  data(parent(Parent,Child)).

parent(Parent,Child)):- %義理の親子関係
  data(parent(Parent,Child2)),
  (data(married_couple(Child2,Child));data(married_couple(Child,Child2))).

「;(OR)」を使っているので少々汚い気もしますが、これで「ParentとChildは親子関係である」ということが定義できます。ちなみに「;」を使いたくない場合は、

parent(Parent,Child):- %直系の親子
  data(parent(Parent,Child)).

parent(Parent,Child)):- %義理の親子関係
  data(parent(Parent,Child2)),
  (data(married_couple(Child2,Child)).
parent(Parent,Child)):- %義理の親子関係2
  data(parent(Parent,Child2)),
  data(married_couple(Child,Child2)).

と書けばいいです。ちょっと論理学の話しを書くと、
(A \vee B) \to C
という論理式は、
(A \to C) \wedge (B \to C)
と等しいからです。

次に「嫁を data(wife(XXX))としてメモリに書き込みたい」と考えたとします(嫁ってどうやったらもらえるんでしょうねぇw)。これは、GlobalメモリにWriteアクセスするのでProcedureとして実装します。それは以下のようになります。

assert_wife:-
  data(male(Husband)),!,
  findall(wife(Wife),data(married_coule(Husband,Wife)),WifeList),!,
  maplist(assert,WifeList).

これによって嫁を全て「data(wife(XXX))」の形で書き込むことができます。

つぎに、「Xの妻Yは何かを返す関数を作りたい」と考えてみます。「Functionは処理に必要なデータを全て引数に持つ」という事が前提となります。Globalメモリの内容でコードが左右されないようにするためです。そこで、「married_couple(XXX,YYY)のような婚姻関係のデータを全て引数にMarriedListとして持つ」とします。そうしたとき、コードは以下のようになります。

function_wife(MarriedList,Husband,WifeList):-
  findall(Wife,member(data(married_couple(Husband,Wife)),WifeList).

これは、処理に必要なデータを「Married_List」として引数に持ち、それのなかからHusbandのWifeをfindallで集めています。この述語は
WifeList=function_wife(MarriedList,Husband)
と関数形式で表せます。
また、Globalメモリにアクセスしないので、どんなプログラムにペーストしても「MarriedList」のデータさえ形式に合っていれば実行可能なプログラムです。
このような関数を実際に使用するときは以下のようなコードになるでしょう。

procedure_wife(X,Y):-
  findall(data(married_couple(A,B)),
    data(married_couple(A,B)),
    MarriedCoupleList),
  function_wife(MarriedCoupleList,X,Y).

このように、Function、Predicate、Procedure、の3種類に分類しながらプログラムを書けばPrologのコードはきわめて保守性の高いコードになるものと思います。