はじめに
Erlang の見た目が Prolog に似ているのは古臭いので現代的に変化させたいということから Elixir が生まれました。
Elixir の変数は新たな変数が定義されるとラムダ計算の let 束縛に似た(しかし、スコープは作らない)処理を行うので上書きを行えます。
Prolog も同様にプログラムを書いていると、変数の上書きができなくて不便さを感じることがあります。
また、変数に大文字を使わなくてはならない点も不満に感じることが多いと思います。
そこで Prolog の変数定義を $name と書くことにすることで、変数を定義し、変数参照は name と書くことで変数を上書きし続けられかつ、
大文字も小文字も関係なく変数を使うことができる仕組みを作ってみましたので提案します。
使用例
プログラムの例は以下のとおりです:
% test.npl
add($r,$a,$b) :- r is a+b.
main :-
add($r,1,2),
add($r,r,3),
add($r,r,4),
(r < 10 -> writeln(a:r) ; writeln(b:r)),
(r >= 10 -> writeln(a:r) ; writeln(b:r)).
:- main.
:- halt.
add/3
述語ではヘッド部の $r
,$a
,$b
により変数 r
a
b
を導入しています。
main/0
内では add/3
の呼び出しの連続で変数 r
が上書きされていきます。
このような仕様にした理由は、変数を導入の構文をできるだけ短くすることを考えると、 $
を一文字付けるのが最適だからです。
一階述語論理では forall(r,a,b)->add(r,a,b):-r is a+b
のように記述して変数を導入しますがこれだと、12文字必要になります。
一般的に n*2+8
文字追加しなければなりません。しかし、$を付けるだけならば、n
だけ増やせば良いことになります。
r,a,b-add(r,a,b):-r is a+b
のように書くとした場合は n*2
だけ書く必要があり最適ではありません。
実装
次に実装を示します:
% newpl.pl
read_file_terms(F,R) :-
open(F,read,FP),read_terms(FP,R,[]),close(FP).
read_terms(FP, Terms, Tail) :-
read_term(FP, C1, [variable_names(Vs)]),maplist(call,Vs),
read_terms(C1, FP, Terms, Tail).
read_terms(end_of_file, _, Tail, Tail) :- !.
read_terms(C, FP, [C|T], Tail) :-
read_term(FP, C2, [variable_names(Vs)]),maplist(call,Vs),
read_terms(C2, FP, T, Tail).
% 述語1つを変換
conv_terms(_,[],[],I,I) :- !.
conv_terms(E,[C|Cs],[C2|Cs2],I,I2) :- conv_goal(E,C,C2,I,I1),conv_terms(E,Cs,Cs2,I1,I2).
conv_goal(E,V,V2,I,I) :- atom(V),memberchk(V=V2,E),!.
conv_goal(_,$V2,V3,I,I) :- memberchk(V2=V3,I),!.
conv_goal(_,$V2,V3,I,[V2=V3|I]).
conv_goal(_,V,V,I,I) :- atomic(V),!.
conv_goal(E,V,V2,I,I2) :- V=..[C|Cs], conv_terms(E,Cs,Cs2,I,I2), V2=..[C|Cs2].
simpl(I1,I2) :- simpl([],I1,I2).
simpl(E,[],E).
simpl(E,[X=_|I1],I2) :- memberchk(X=_,E),simpl(E,I1,I2).
simpl(E,[V|I1],I2) :- simpl([V|E],I1,I2).
merge(S1,[],S1).
merge(S1,[X=V|S2],S3) :- member(X=V,S1),merge(S1,S2,S3).
merge(S1,[X=V|S2],S3) :- merge([X=V|S1],S2,S3).
conv_goals((A,B),(A1,B1)) --> conv_goals(A,A1),conv_goals(B,B1).
conv_goals((A;B),(A1;B1),I,I3) :-
conv_goal(I,A,A1,[],I1),
conv_goal(I,B,B1,[],I2),
simpl(I1,S1),simpl(I2,S2),
merge(S1,S2,S3),
append(S3,I,I3).
conv_goals(A,A1,I,I2) :- conv_goal(I,A,A1,[],I1),append(I1,I,I2).
% 文を変換
conv_stmt((:- V),(:- V2)) :- !,conv_goals(V,V2,[],_).
conv_stmt((H :- V),(H2 :- V2)) :- !,conv_goals((H,V),(H2,V2),[],_).
conv_stmt(V,V2) :- !,conv_goals(V,V2,[],_).
% 文のリストを変換
conv_stmts(Vs,Vs2) :- maplist(conv_stmt,Vs,Vs2).
assert1(:- V) :- call(V).
assert1(V) :- assertz(V).
consult1(F) :- read_file_terms(F,R),conv_stmts(R,R2),maplist(assert1,R2).
:- current_prolog_flag(argv, Argv),maplist(consult1,Argv).
この実装は、基本的に処理系のパーサをそのまま用いています。大文字の変数は一度変数から atom に変換してしまいます。
その後 $
によって変数を新たに追加するような処理を行っています。
実行と結果
$ swipl.pl newpl.pl test.npl
b:10
a:10
うまく動作しています。
今後の課題
変数に大文字を使った例:
% eval.npl
eval($Γ,$x,$v) :- member(x=v,Γ).
eval($Γ,$i,$i) :- integer(i).
eval($Γ,$a+ $b,$v) :- eval(Γ,a,$v1),eval(Γ,b,$v2), v is v1+v2.
eval($Γ,$a* $b,$v) :- eval(Γ,a,$v1),eval(Γ,b,$v2), v is v1*v2.
eval($Γ,$x= $a;$b,$v) :- eval(Γ,a,$v1),eval([x=v1|Γ],b,v).
:- eval([],x=1*2+3*4;x,$r),writeln(r).
:- halt.
$ swipl.pl newpl.pl eval.npl
14
Γ
を変数として導入しています。残念なのは $
と +
をつなげて書くことができないことや、$v
の変数を返す場合に変数の宣言を述語のヘッド部分で行うために、どんどん書き換えて出力すると言うことができないところです。
改良することを考えると、 _name
を入力 name_
を出力することで以下のようにかけるようにするとよいかもしれません:
% eval.npl
eval(_Γ,_x,_v) :- member(x=v,Γ).
eval(_Γ,_i,_i) :- integer(i).
eval(_Γ,_a+_b,_v) :- eval(Γ,a,_v1),eval(Γ,b,_v2), v is v1+v2.
eval(_Γ,_a*_b,_v) :- eval(Γ,a,_v1),eval(Γ,b,_v2), v is v1*v2.
eval(_Γ,_x=_a;_b,v_) :- eval(Γ,a,_v),eval([x=v|Γ],b,_v).
:- eval([],x=1*2+3*4;x,_r),writeln(r).
:- halt.
v_
は ヘッド内で用いて最後の v
の変数を参照することにすれば、より美しく記述可能となるでしょう。
まとめ
Prolog の新しい変数導入方法を考え実装をしました。
比較的短いソースで便利な機能を Prolog に導入し変数が大文字という古臭く不便に感じる部分を現代的にすることが出来ました。