M.Hiroi's Home Page

Prolog Programming

お気楽 Prolog プログラミング入門

[ PrevPage | Prolog | NextPage ]

有理数

SWI-Prolog は「有理数 (rational)」を扱うことができます。SWI-Prolog の場合、有理数 p/q を prq と書きます。/ のかわりに r を使います。または、関数 rdiv(p, q) や関数 rational(number) で有理数を生成することができます。

簡単な例を示しましょう。

?- N is 1r2.
N = 1r2.

?- N is rdiv(1, 3).
N = 1r3.

?- N is rational(1.5).
N = 3r2.

?-

●有理数の判定

有理数の判定は述語 rational/1, rational/3 で行うことができます。

rational(R).
rational(R, P, Q).

rational/1 は引数 R が整数または有理数ならば成功します。rational/3 は引数 R が整数または有理数で、分子が P とマッチングし、分母が Q とマッチングすれば成功します。有理数 R を分子と分母に分解するのに使えます。また、有理数 R の分子 p は関数 numerator(R) で、分母は関数 denominator(R) で求めることができます。

簡単な使用例を示します。

?- rational(1r2).
true.

?- rational(1).
true.

?- rational(1.5).
false.

?- rational(1r3, P, Q).
P = 1,
Q = 3.

?- rational(1r3, 1, Q).
Q = 3.

?- rational(1r3, 2, Q).
false.

?- N is numerator(1r2).
N = 1.

?- N is denominator(1r2).
N = 2.

?-

●四則演算と比較演算子

整数や浮動小数点数と同じように、有理数は算術演算子や比較演算子を使用することができます。

?- N is 1r2 + 1r3.
N = 5r6.

?- N is 1r2 - 1r3.
N = 1r6.

?- N is 1r2 * 1r3.
N = 1r6.

?- N is 1r2 / 1r3.
N = 3r2.

?- 1r3 < 1r2.
true.

?- 1r3 > 1r2.
false.

?- 1r3 =:= 1r2.
false.

?- 1r2 =:= 1r2.
true.

?- 1r3 =\= 1r2.
true.

?-

整数と有理数または有理数と有理数を演算したとき、結果が整数になるならば整数に、そうでなければ有理数になります、浮動小数点数と有理数の計算結果は浮動小数点数になります。

?- N is 1r2 + 1r2.
N = 1.

?- N is 1r2 + 1.
N = 3r2.

?- N is 1r2 + 1.5.
N = 2.0.

●パズル「小町分数」

それでは簡単な例題として、パズル「小町分数」を解くプログラムを作りましょう。

[問題] 小町分数

下図の A から I の場所に 1 から 9 までの数字をひとつずつ配置します。3 つの分数を足すと 1 / N になる配置を求めてください。

\( \dfrac{A}{BC} + \dfrac{D}{EF} + \dfrac{G}{HI} = \dfrac{1}{N} \)

ex)
3 / 27 + 6 / 54 + 9 / 81 = 1 / 3
3 / 54 + 6 / 72 + 9 / 81 = 1 / 4

図 : 小町分数

このパズルの元ネタは N = 1 の場合で、参考文献 [1] に掲載されています。

プログラムは次のようになります。

リスト : 小町分数

komachi :-
    permutation([1,2,3,4,5,6,7,8,9], [A,B,C,D,E,F,G,H,I]),
    A < D,
    D < G,
    X1 is B * 10 + C,
    X2 is E * 10 + F,
    X3 is H * 10 + I,
    Z is rdiv(A, X1) + rdiv(D, X2) + rdiv(G, X3),
    rational(Z, 1, N),
    format('~d/~d~d + ~d/~d~d + ~d/~d~d = 1/~d~n', [A,B,C,D,E,F,G,H,I,N]),
    fail.

単純な生成検定法です。重複解を排除するため、A < D < G の条件を付けています。また、順列を生成するとき、このチェックを入れることで枝刈りと同じ効果を得ることができます。興味のある方は試してみてください。実行結果は次のようになります。

?- komachi.
1/24 + 3/56 + 7/98 = 1/6
1/26 + 5/39 + 7/84 = 1/4
1/32 + 5/96 + 7/84 = 1/6
1/38 + 2/95 + 4/76 = 1/10
1/48 + 5/32 + 7/96 = 1/4
1/56 + 3/72 + 9/84 = 1/6
1/96 + 5/32 + 7/84 = 1/4
1/96 + 5/48 + 7/32 = 1/3
2/18 + 5/63 + 7/49 = 1/3
2/19 + 4/57 + 6/38 = 1/3
3/27 + 6/54 + 9/81 = 1/3
3/48 + 5/16 + 9/72 = 1/2
3/54 + 6/72 + 9/81 = 1/4
5/34 + 7/68 + 9/12 = 1/1
false.

?-

結果は全部で 14 通りになりました。

●単位分数の和

パズルではありませんが、分数のお話を紹介します。分子が 1 の分数を「単位分数」といいますが、単位分数の和は古代エジプト人がよく研究していたそうです。この話は M.Kamada さん からお聞きしたのですが、参考文献 [2] に「分数を単位分数の和で表す方法」がありましたので紹介します。

0 < m / n < 1 の分数を単位分数の和で表します。まず、n / m の商 q を求めます。もし、割り切れれば単位分数になりますね。そうでなければ、m / n から 1 / (q + 1) を引き算して M / N を求めます。あとは、M / N に対して同じ処理を繰り返すだけです。次の式を見てください。

M / N = m / n - 1 / (q + 1)
M / N = (m(q + 1) - n) / n(q + 1)
M = m(q + 1) - n = m - (n - mq) = m - (n % m)

0 < (n % m) < m ですから、M は必ず m より小さな値になります。つまり、この処理を繰り返していくと m は必ず 1 になるので、分数を単位分数の和で表すことができる、というわけです。なるほど納得のアルゴリズムですね。たとえば、11 / 13 を単位分数の和で表してみましょう。

11 / 13 => q = 1, 11 / 13 - 1 / 2 = 9 / 26
 9 / 26 => q = 2,  9 / 26 - 1 / 3 = 1 / 78
=> 11 / 13 = 1 / 2 + 1 / 3 + 1 / 78

このように、分子 m の値は減少していきます。このアルゴリズムを Prolog でプログラムすると、次のようになります。

リスト : 分数を単位分数の和で表す

bunsu(M, N) :-
    N mod M =:= 0, A is N // M, format('1/~d~n', A), !.
bunsu(M, N) :-
    Q is N // M + 1,
    format('1/~d + ', Q),
    A is M * Q - N,
    B is N * Q,
    bunsu(A, B).

述語名は適当なので、ふさわしい名前に変更してください。あとは、アルゴリズムをそのままプログラムしただけなので、特に難しいところはないでしょう。それでは実行してみましょう。

?- bunsu(11, 13).
1/2 + 1/3 + 1/78
true.

?- bunsu(12, 13).
1/2 + 1/3 + 1/12 + 1/156
true.

?- bunsu(19, 23).
1/2 + 1/4 + 1/14 + 1/215 + 1/138460
true.

?-

このほかにも、単位分数の和で表す方法は何通りもあるわけで、この方法はその中のひとつにすぎません。古代エジプトではどのような方法で求めたのでしょうか。興味深いところです。

-- 参考文献 ------
[1] 芦ヶ原伸之,『超々難問数理パズル 解けるものなら解いてごらん』, 講談社, 2002
[2] 奥村晴彦, 『C言語による最新アルゴリズム事典』, 技術評論社, 1991

ファイル入出力

●ファイル入出力の基本

今回は Prolog のファイル入出力について説明します。Prolog の標準とされている処理系にエジンバラ Prolog があります。エジンバラ Prolog では、入出力用に 2 つのストリーム (stream) が用意されています。

ストリームは「流れ」や「小川」という意味ですが、プログラミング言語の場合は「ファイルとプログラムの間でやりとりされるデータの流れ」という意味で使われています。ストリームはファイルと 1 対 1 に対応していて、ファイルからデータを入力する場合は、ストリームを経由してデータが渡されます。逆に、ファイルへデータを出力するときも、ストリームを経由して行われます。

ここでは、入力ストリームを current input と呼び、出力ストリームを current output と呼ぶことにします。デフォルトでは、current input が標準入力、current output が標準出力に設定されています。Prolog のファイル入出力は、current input と current output の設定をファイルへ切り替えることで行います。次の表を見てください。

表 : ファイル入出力
述語名機能
see(File)ファイル File を入力用にオープンして current input に設定する
seencurrent input に設定されているストリーム(ファイル)をクローズする
seeing(File)current input に設定されているストリームを求める
tell(File)ファイル File を出力用にオープンして current output に設定する
toldcurrent output に設定されているストリーム(ファイル)をクローズする
telling(File)current output に設定されているストリームを求める

ファイル名の指定にはアトムを使います。ファイル名にアトムで使用できない文字が含まれている場合には、ファイル名をシングルクオート ( ' ) で囲んでください。たとえば、ピリオド ( . ) を含むファイル名 test.dat は 'test.dat' と指定してください。また、特別に user というファイル名を指定すると標準入出力が対象になります。

ファイルを入力用にオープンするには述語 see(File) を使います。引数 File にはファイル名を指定します。これで current input は File で指定されたファイルになります。あとは、入力用の述語 read を使って、ファイルからデータを読み込むことができます。なお、see でファイルをオープンする場合、ファイルが存在しないとエラーになります。

ファイルを出力用にオープンするには述語 tell(File) を使います。これで current output は File で指定されたファイルになります。あとは、出力用の述語 write や format を使って、データをファイルへ出力することができます。tell でファイルをオープンする場合、ファイルが存在しない場合は新しいファイルを作成します。ファイルが存在する場合は、ファイルを切り詰めてから(ファイルサイズを 0 にしてから)データを書き込みます。既存のファイルは、内容が破壊されることに注意してください。

オープンしたファイルはクローズしないといけません。入力用のファイルは述語 seen で、出力用のファイルは述語 told でクローズします。また、current input と current output に設定されているストリームは述語 seeing(File) と telling(File) で求めることができます。なお、SWI-Prolog の場合、これらの述語でファイル名を求めることはできません。

簡単な使用例を示しましょう。ファイルからデータを読み込んで表示するプログラムです。

リスト : データファイルの表示

write_data(end_of_file).
write_data(X) :- format('~a.~n', X).

read_data(X) :-
    see(X),
    repeat,
    read(Y),
    write_data(Y),
    Y == end_of_file,
    !,
    seen.

述語 read_data(X) は、引数 X で指定されたファイルからデータを読み込んで表示します。データの入力には述語 read を用いるので、データはピリオド (.) で区切ってください。たとえば、test.dat には次のようなデータが格納されているとします。

prolog.
programming.
siliconvalley.
oakland.

最初にファイルを see でオープンします。これで述語 read を使って、ファイルからデータを読み込むことができます。あとは、ファイルの終了 (end_of_file) になるまで、ファイルからデータを読み込んで write_data で出力します。repeat と Y == end_of_file で、失敗駆動ループを構成していることに注意してください。write_data は format でデータを出力するだけですが、最初の節でデータが end_of_file の場合は出力しないようにしています。最後に、seen でファイルをクローズします。

実行例は次のようになります。

?- read_data('test.dat').
prolog.
programming.
siliconvalley.
oakland.
true.

?-

きちんと動作していますね。もしも、ほかのファイルへ出力したい場合は、次のようにプログラムすればいいでしょう。

リスト : データファイルのコピー

copy_data(X, Y) :-
    see(X),
    tell(Y),
    repeat,
    read(Z),
    write_data(Z),
    Z == end_of_file,
    !,
    seen,
    told.

copy_data の引数 X が入力ファイルで、Y が出力ファイルを表します。入力ファイルは see でオープンし、出力ファイルは tell でオープンします。これで、write や format の出力はファイル Y へ書き込まれます。ファイルの読み込みと書き込みは read_data と同じです。あとは、seen と told でファイルをクローズするだけです。

それでは実行例を示します。

?- copy_data('test.dat','test1.dat').
true.

?- read_data('test1.dat').
prolog.
programming.
siliconvalley.
oakland.
true.

?-

test.dat の内容は test1.dat にコピーされています。正常に動作していますね。

●バイト単位の入出力

入出力用の述語は read や write のほかに、バイト単位で行う get や put もあります。次の表を見てください。

表 : 入出力用の述語
述語名機能
get(C)空白文字以外の 1 文字を入力する
get0(C)1 文字入力する
put(C)1 文字出力する

これらの述語の引数 C は、文字を表す ASCII CODE (数値) です。get と get0 の場合、ファイルの終了は -1 になります。簡単な例を示しましょう。

?- get(C).
|: a

C = 97.

?- get(C).
|:  <- 空白を入力
|: b

C = 98.

?- get0(C).
|:  <- 空白を入力

C = 32.

?- put(97).
a
true.

?-

get0 と put を使うと、ファイルの内容を表示するプログラムを作ることができます。次のリストを見てください。

リスト : ファイルの内容を表示

write_code(-1).
write_code(C) :- put(C).

type_file(X) :-
    see(X),
    repeat,
    get0(C),
    write_code(C),
    C == -1,
    !,
    seen.
?- type_file('test.dat').
prolog.
programming.
siliconvalley.
oakland.
true.

?-

type_file ではファイルから get0 で 1 文字読み込み、write_code では put で 1 文字出力します。ファイルの終了は -1 なので、repeat と C == -1 で失敗駆動ループを構成しています。また、write_code でも最初の節で -1 を出力しないようにしています。これでファイルの内容を表示することができます。

●ファイル入出力の切り替え

述語 see と tell はファイルをオープンするだけではなく、current input と current output の設定を他のファイルへ切り替えることができます。この機能を使って、複数のファイルを扱うことができます。

SWI-Prolog の場合、see や tell でファイルをオープンすると、そのファイルに対応したストリームが作成され、それが current input や current output に設定されます。このストリームは述語 seeing と telling で求めることができます。そして、see や tell の引数にストリームを与えると、current input や current output の設定をそのストリームに変更することができます。

たとえば、2 つのファイル A, B からデータを読み込む場合を考えてみましょう。最初に、see(A) でファイル A をオープンし、seeing(As) でストリームを As に求めます。同様に、see(B), seeing(Bs) でファイル B をオープンしてストリームを Bs に求めます。

次に、see(As) を実行すると、current input の設定はファイル A のストリームになります。ここで read を実行すると、ファイル A からデータを読み込みます。その次に see(Bs) を実行すると、current input はファイル B のストリームに設定されるので、read を実行するとファイル B からデータを読み込むことができます。

Prolog の場合、see や tell でオープンしたファイルは、seen や told でクローズしない限り継続して使用できるので、current input や current output を切り替えたあとでも、引き続きデータの読み込みや書き込みを行うことができます。

簡単な例題として、2 つのファイルからデータを入力して、1 つのファイルへデータを出力するプログラムを作ってみましょう。次の図を見てください。

入力ファイル
test2.dat    test3.dat
---------    ---------
abc.         100.
def.         200.
ghi.         300.

出力ファイル
test4.dat
---------
abc. 100.
def. 200.
ghi. 300.

ファイル test2.dat と test3.dat からデータをひとつずつ読み込み、それらのデータをファイル test4.dat に書き込みます。プログラムは次のようになります。

リスト : データファイルの連結

write_data(end_of_file, _).
write_data(X, Y) :- format('~a. ~a.~n', [X, Y]).

paste_data(X, Y, Z) :-
    see(X), seeing(Xs),
    see(Y), seeing(Ys),
    tell(Z),
    repeat,
    see(Xs),
    read(Xd),
    see(Ys),
    read(Yd),
    write_data(Xd, Yd),
    Xd == end_of_file,
    !,
    see(Xs), seen,
    see(Ys), seen,
    told.

述語 paste_data の引数 X, Y が入力ファイル、Z が出力ファイルを表します。最初に、see と seeing で入力ファイルをオープンして、そのストリームを Xs と Ys に求めます。そして、出力ファイル Z を tell でオープンします。

次に、入力ファイルからデータを読み込みます。see(Xs) で current input をファイル X に切り替えて、データを read(Xd) で 1 つ読み込みます。同様に、see(Ys), read(Yd) でファイル Y からデータを 1 つ読み込みます。このように see で current input の設定を切り替えることで、複数のファイルからデータを入力することができます。

あとは write_data で読み込んだデータをファイル Z へ出力します。最後に、seen と told でファイルをクローズします。なお、このプログラムはファイル終了のチェックをファイル X でしか行っていません。もしも、ファイル X のデータ数がファイル Y よりも多い場合は、ファイル X のデータと end_of_file が書き込まれることになります。ご注意ください。

それでは実行例を示しましょう。次のように paste_data を実行すると、test2.dat と test3.dat のデータが test4.dat に書き込まれます。

?- paste_data('test2.dat','test3.dat','test4.dat').
true.

?- ^D
% halt
$ cat test4.dat
abc. 100.
def. 200.
ghi. 300.

●プログラムの読み込み

Prolog には、ファイルからプログラムを読み込む述語 consult(File) が用意されています。プログラムの読み込みは、リストの表記法 [File]. でも行うことができます。複数のプログラムをまとめて読み込むときは、リストの表記法を使った方が便利でしょう。

cosult(FileA),cosult(FileB),cosult(FileC). ==> [FileA, FileB, FileC].

標準的な Prolog の場合、プログラムを読み込む述語に consult と reconsult があります。consult は単純にプログラムを追加していくので、同じプログラムが複数定義されることがあります。たとえば、プログラムに foo(a, b). が定義されていて、同じプログラムを再度 consult で読み込むと、foo(a, b) の定義が 2 つに増えるのです。

reconsult の場合、重複するプログラムは更新するだけなので、同じ定義が 2 つに増えることはありません。SWI-Prolog の場合、consult は reconsult のように動作するので注意してください。

簡単な例を示しましょう。次のプログラムを読み込みます。

foo(a, b).
foo(c, d).
foo(e, f).

ファイル名を test5.pl とし、SWI-Prolog でファイルを読み込みます。

?- [test5].
true.

?- listing(foo).
foo(a, b).
foo(c, d).
foo(e, f).

true.

?- [test5].
true.

?- listing(foo).
foo(a, b).
foo(c, d).
foo(e, f).

true.

?-

このように、test5.pl を 2 度読み込んでも、同じ定義が 2 つに増えることはありません。一般的な Prolog の場合、consult でプログラムを読み込むと、次のように foo の定義が増えてしまいます。

| ?-listing(foo).
foo(a,b).
foo(c,d).
foo(e,f).
foo(a,b).
foo(c,d).
foo(e,f).
yes

このような場合は reconsult でプログラムを読み込んでください。

●端末からプログラムを入力する

SWI-Prolog で端末から事実や規則を入力したい場合は、consult や [ ] などプログラムを読み込む述語に user を指定します。user は標準入出力を指定する特別なファイル名です。

簡単な実行例を示します。入力の終了は ^D (Ctrl + d) です。Windows では ^Z (Ctrl + z) でもかまいません。

?- [user].
|: foo(a, b).
|: foo(c, d).
|: ^D
true.

?- foo(X, Y).
X = a,
Y = b ;
X = c,
Y = d.

?-

このように、ファイル名を user に設定すると、端末から事実や規則を入力することができます。


初版 2004 年 6 月 9 日
改訂 2023 年 5 月 7 日

Copyright (C) 2004-2023 Makoto Hiroi
All rights reserved.

[ PrevPage | Prolog | NextPage ]