TyRuBa Dance

TyRuBa」 というプログラミング言語について調べてみます。Prolog などに代表される論理型言語の一種で、この言語の特徴としては、 デザインパターンなどの抽象的な概念を記述して、それをもとに Javaのコードを生成する、という目的があるようです。 まだ実験的な言語らしいですが、面白そうなのでいじってしまいます。

いつも通り、間違いの指摘大歓迎。

  1. インストール
  2. * 論理型言語とは
  3. 例1
  4. List, Compound Term
  5. 例2
  6. * 双方向性
  7. 手続き型的な見方
  8. 例3
  9. or, ==, NOT, FINDALL, TEST, ...
  10. 例4:高階述語
  11. TyNI
  12. 例5:ライブラリ
  13. その他のトピックス
  14. * Javaのコード生成

インストール (2003/06/15)

とりあえず実行にはJavaが必要です。お持ちでない方は J2SE の導入を。

TyRuBa本体は、プロジェクトのページ の Download というリンクをたどってダウンロードできます。compiled と付いてる方のアーカイブを落とした方が簡単なのでそちらがオススメ。 Javaアプリなので使っているOS等は気にする必要ありません。

ダウンロードしたら解凍してすぐ使えます。Unix系の環境なら、 binディレクトリに入ってるtyruba.sh を TyRuBa のインストール位置に合わせて書き換えればこのスクリプトから起動できます。 Windowsでも、shを見ながら適当にbatファイルを書いておくと楽です。 あるいは、私が適当に書いたtyruba.bat の-classpathを書き換えて使ってやってください。

このtyruba.shなりtyruba.batなりにパスを通しておくと、

tyruba -i
tyruba ソースファイル名 -i

で起動できます。ソースファイルの拡張子は .rub が標準だそうな。

論理型とは? (2003/06/16)

fact / query

C:\Documents and Settings\K.INABA\Desktop> tyruba -i
Option -i seen...
INCLUDING jar:file:/C:/usr/develop/TyRuBa/dist/tyruba.jar!/lib/prolog.rub
INCLUDING ...
...(略)...
 Done

--- Interactive mode... type queries!
end with CTRL-D

起動したらとりあえず↑こんなのが出ます。 ここにプログラムを打ち込むと解釈して実行してくれます。 何か非常に簡単なプログラムを書いてみましょう。 早速Hello, World...と行きたいのですが、HelloWorld を書いてもこの言語の特徴は全く表せない気がするのでもっと単純な例で。

human(Taro).
** assertion added to rulebase **
human(Jiro).
** assertion added to rulebase **
human(Hanako).
** assertion added to rulebase **

TyRuBaのプログラムは基本的に "Rule" と "Query" のみで出来上がります。 上で打ち込んだ human(taro). は Rule の一種で、 「太郎は人間である」という"fact"(事実)を宣言しています。 続いて「次郎は人間である」と「花子は人間である」と高らかに歌い上げると、 そのたびに内部のルールデータベースに追加されていきます。

:- human(Taro).
##QUERY : human<Taro>
.
| SUCCESS |
##END QUERY
:- human(Hanako).
##QUERY : human<Hanako>
.
| SUCCESS |
##END QUERY
^D

"Query" の例を見ていきましょう。一行目の :- human(taro). のように :- から始まるのがQueryです。意味的には、 「太郎は人間です…か?」という"問い合わせ"になっています。

既にデータベースには「太郎は人間」と覚えてあるので、このQueryは成功しました。 要するに「太郎は人間です」とTyRuBaから返事が返ってきたというわけです。 同様に花子で試してみてもOk。

:- human(Saburo).
##QUERY : human<Saburo>
.
FAILURE
##END QUERY

三郎についてはデータベースに何も入っていないので、QUERY失敗。

変数

:- human(?x).
##QUERY : human<?x>
.
| ?x=Jiro |
| ?x=Taro |
| ?x=Hanako |
##END QUERY

頭に?をつけておくと変数になります。 で、「?x は人間ですか?」と問い合わせると、 そのQUERYが成功するような ?x を勝手に探してきてくれます。 つまり、太郎と次郎と花子ですね。

rule

「○○は△△である」という宣言の他に、Rule としては 「○○が△△ならば□□は☆☆である」という形のものも書けます。

--- Interactive mode... type queries!
end with CTRL-D
dog(Pochi).
** assertion added to rulebase **
dog(Kuro).
** assertion added to rulebase **
dog(John).
** assertion added to rulebase **
cat(Tama).
** assertion added to rulebase **
cat(Mike).
** assertion added to rulebase **
barks(?x) :- dog(?x).
** assertion added to rulebase **
:- barks(Tama).
##QUERY : barks<Tama>
..
FAILURE
##END QUERY
:- barks(Kuro).
##QUERY : barks<Kuro>
.o
| SUCCESS |
##END QUERY
:- barks(?x).
##QUERY : barks<?x>
.o
| ?x=John |
| ?x=Kuro |
| ?x=Pochi |
##END QUERY

強調部分で「?xが犬なら、?xは吠えます」と宣言しています。 A :- B は「A if B」みたいに読めます。 :- barks(?x). という問い合わせを受けるとTyRuBaは、

「?xは吠えるらしい」「吠える物って何だ?」 「お、barks(?x) :- dog(?x)というルールがある」 「ということは、?xが犬なら?xは確実に吠える」「じゃあ犬な物ってなんだ?」 「dog(Pochi) と書いてある。Pochiは犬だ。なのでPochiは吠える」 「あ、他に dog(Kuro) とも書いてある。Kuroも犬だ。Kuroも吠える」 …

と論理的に考えて、答えを見つけてきます。勿論、 犬以外にも例えば狼が吠えるかもしれませんが、 そういうことはルールのデータベースに書いてないので、考えません。

というわけで

この「○○である」と「○○であるならば△△である」を大量に並べてから変数入りで Queryすると、条件を満たす解を見つけてきてくれる、 というのが基本的な構造になります。

例1 (2003/06/16)

ruleに書くhumanとかdogとかbarks とかは「述語」と呼ばれます。で、この述語、上の例では全部一引数でしたが、 二つでも三つでもOK。「○は△の□である」とか「☆は▽を◇したもの」 みたいな関係を表すときは二引数になりますし、 場合によってはもっと増えることもよくあります。

// test.rub
// コメントは//とか/*~*/で。

father(Namihei, Sazae).
father(Namihei, Katsuo).
father(Namihei, Wakame).
mother(Fune, Sazae).
mother(Fune, Katsuo).
mother(Fune, Wakame).
father(Masuo, Ikura).
mother(Sazae, Ikura).

// 父なら親である。母なら親である。と二種類以上条件を二つ並べるのも可
parent(?x, ?y) :- father(?x, ?y).
parent(?x, ?y) :- mother(?x, ?y).

child(?x, ?y) :- parent(?y, ?x).

// ?zの父親が?xで、しかも、?yの親がその?zなら、?xは?yの祖父。
// ?zみたいに作業用の一時的な変数を使うことができます。
// あと、カンマ区切りで条件を並べると、&の意味。右辺が両方成り立てば、左辺も成り立つ、と。
grandfather(?x, ?y) :- father(?x, ?z), parent(?z, ?y).
grandmother(?x, ?y) :- mother(?x, ?z), parent(?z, ?y).

grandparent(?x, ?y) :- grandfather(?x, ?y).
grandparent(?x, ?y) :- grandmother(?x, ?y).

grandchild(?x, ?y) :- grandparent(?y, ?x).

sibling(?x, ?y) :- child(?x, ?z), child(?y, ?z).

祖父・孫関係にある二人を探す。

>> tyruba test.rub -i
...略...
:- grandparent(?x, ?y).
##QUERY : grandparent<?x,?y>
.ooHoooooooooooo
| ?y=Ikura ?x=Fune |HHHHH
| ?y=Ikura ?x=Namihei |
##END QUERY

誰かの子供な人を探す。変数名を ? 一文字にするとその部分の探索結果は表示されません。

:- child(?x, ?).
##QUERY : child<?x,?>
.H
| ?x=Katsuo |
| ?x=Wakame |
| ?x=Ikura |
| ?x=Sazae |
##END QUERY

Queryでも、カンマ区切りで複数個の条件を書くことができます。 誰かの子供であって、しかも誰かの母親な人。

:- child(?x, ?), mother(?x, ?).
##QUERY : child<?x,?>,mother<?x,?>
H.ooo
| ?x=Sazae |
##END QUERY

二個の ?x は同一視されますが、特別な変数 ? は何個あっても同一視されないことに注意。これを例えば :- child(?x, ?y), mother(?x, ?y). とするとFAILUREしてしまいます。Erlangの記事 を読まれた方向けに説明するならば、パターンマッチの時の _ と一緒です。一緒ですというか、「論理型言語というのは 要するにひたすらパターンマッチを繰り返す言語である」 と考えてしまっても結構差し支えない位の勢いです。

List, Compound Term (2003/06/17)

Taro とか Pochi みたいな一つの"名前"だけでなくて、 もう少し構造をもったデータもRuleやQueryに使うことができます。 名付けて、Compound Term。日本語で言うと複合項?

birthday( Taro, date(March, 6) ).
birthday( Jiro, date(October, 21) ).
birthday( Hanako, date(April, 10) ).

この場合 date(月, 日) というCompound Termを使っています。 何か問い合わせてみます。

:- birthday( Hanako, ?day ).
##QUERY : birthday<Hanako,?day>
.
| ?day=date<April,10> |
##END QUERY

「花子さんの誕生日は?」「4月10日」

:- birthday( ?x, date(October, ?) ).
##QUERY : birthday<?x,date<October,?>>
.
| ?x=Jiro |
##END QUERY

dateの中身を変数にして見に行くことも可能です。 「10月生まれの人は?」「次郎君」

Compound Term は date<March, 6> と、 とがった括弧で書いてもよいらしいです。 特に述語と区別をつけたい場合はこちらの方がベターかも。

List

可変長のデータを扱うための特別な構造として、"リスト" が用意されています。

evennumber_list( [2,4,6,8,10] ).
oddnumber_list( [1,3,5,7,9] ).
:- oddnumber_list( [1,3,5,7,9] ).
##QUERY : oddnumber_list<[1,3,5,7,9]>
.
| SUCCESS |
##END QUERY
:- evennumber_list( [?a,?b,?c,?d,?e] ).
##QUERY : evennumber_list<[?a,?b,?c,?d,?e]>
.
| ?e=10 ?d=8 ?c=6 ?b=4 ?a=2 |
##END QUERY

「[1,3,5,7,9]は奇数リストですか?」「いえーす」などなど。 []の間に要素を並べるとリストができます。 …としかし、これではまだ可変長リストは扱えません。

:- oddnumber_list( [?a,?b,?c,?d] ).
##QUERY : oddnumber_list<[?a,?b,?c,?d]>
.
FAILURE
##END QUERY

ルールの方が5個のリストで宣言されているので、 その「5個」という長さがわからないまま適当に Query を出すことができないのです。oddnumber_list( ?x ). みたいにリスト全体と変数?xがマッチするようなQueryは書けますが、 例えば「奇数リストの二番目の要素は?」みたいな問い合わせができません。

:- oddnumber_list( [?head | ?rest] ).
##QUERY : oddnumber_list<[?head|?rest]>
.
| ?head=1 ?rest=[3,5,7,9] |
##END QUERY

そこで、| 記号を使って、リストを「先頭一個」と「残りのリスト」 からなるCompoundTermだと思って扱うことができます。 二番目の要素を扱いたければ、こんな感じ。

:- evennumber_list( [? | [4 | ?]] ).
##QUERY : evennumber_list<[?,4|?]>
.
| SUCCESS |
##END QUERY

「偶数リストの2番目の要素は4ですか?」「はい」

無名CompoundTerm

birthday( Taro, <March, 6> ).
birthday( Jiro, <October, 21> ).
birthday( Hanako, <April, 10> ).

dateとかの名前を付けなくてもよいそうな。

例2 (2003/06/23)

空リストと空リストは、同じリストである。 残りのリスト?r1と?r2が同じならば、先頭要素が同じ?xな二つのリスト [?x|?r1] と [?x|?r2] は同じリストである。と、二つのルールを並べてみました。

is_same_list( [], [] ).
is_same_list( [?x|?r1], [?x|?r2] ) :- is_same_list( ?r1, ?r2 ).

このように、ruleは再帰的に使う(is_same_listの定義の中にis_same_listが入る) こともできます。

:- is_same_list( [1,2,3], [4,5,6] ).
##QUERY : is_same_list<[1,2,3],[4,5,6]>
.
FAILURE
##END QUERY
:- is_same_list( [1,2,3], [1,2,3] ).
##QUERY : is_same_list<[1,2,3],[1,2,3]>
..oo
| SUCCESS |
##END QUERY

append

「リストAのうしろにリストBをつなげたらリストCになる」を表すような述語 append_list(A,B,C) を記述してみましょう。 Aが空リストの場合と、そうでない場合とに分けてルールを書くのが簡単です。

append_list( [], ?r, ?r ).
append_list( [?a|?b], ?r1, [?a|?r2] ) :- append_list( ?b, ?r1, ?r2 ).

じっくりと睨むと、そういう意味になっているのがおわかりいただけるかと。

:- append_list( [1,2], [3,4], [1,2,3,4] ).
##QUERY : append_list<[1,2],[3,4],[1,2,3,4]>
..o
| SUCCESS |
##END QUERY
:- append_list( [1,2], [3,4], [1,2,1,2] ).
##QUERY : append_list<[1,2],[3,4],[1,2,1,2]>
.oo
FAILURE
##END QUERY

ちゃんと期待したとおりに SUCCESSS/FAILURE してくれています。

双方向性 (2003/06/23)

最初の方の例では、:- human(Taro). のようなYesかNoか、 な問い合わせだけでなく、:- barks(?x). 、つまり吠えるような ?x を見つけてきてね、という問い合わせもできることを見ました。 そこで当然、append_list でも変数入り問い合わせをしたくなります。

:- append_list( [1,2], [3,4], ?z ).
##QUERY : append_list<[1,2],[3,4],?z>
.oo
| ?z=[1,2,3,4] |
##END QUERY

「[1,2] のうしろに [3,4] をつなげた物は何ですか?」「[1,2,3,4]です。」

「リストAのうしろにリストBをつなげたらリストCになる」という述語を書いたら、 なんと「リストAとリストBを与えるとその連結Cを返す」という、 Cなど他の言語で言う関数のようなものが出来上がってしまいました。素晴らしい。

// C++で書くと(素直に書きすぎで効率悪いですが)こんなの
list<int> append_list( list<int> lst1, list<int> lst2 ) {
	for(list<int>::iterator i=lst2.begin(), e=lst2.end(); i!=e; ++i)
		lst1.push_back( *i );
	return lst1;
}

…ところでよくよく考えてみると、TyRuBaの場合、append_list の引数を変数 ?zにするのは、別に第三引数である必要はありません。 では第二引数を変数にしてみましょう。

:- append_list( [7,6], ?z, [7,6,5,4,3] ).
##QUERY : append_list<[7,6],?z,[7,6,5,4,3]>
.oo
| ?z=[5,4,3] |
##END QUERY

「[7,6]のうしろにつなげると[7,6,5,4,3]になるようなリストは何?」 「[5,4,3] です」

リストAとリストBを取ってその連結リストCを返す関数(A×B→C)、 と思っていたら、逆向きに、リストAとリストCをとってリストBを返す (B←A×C)という計算もできるようです。 これは論理型言語独特の性質で、双方向性などと呼ばれています。

あ、もちろん、変数を1個に限る必要はありません。

:- append_list( ?x, ?y, [1,2,3] ).
##QUERY : append_list<?x,?y,[1,2,3]>
.ooH
| ?y=[] ?x=[1,2,3] |
| ?y=[3] ?x=[1,2] |
| ?y=[2,3] ?x=[1] |
| ?y=[1,2,3] ?x=[] |
##END QUERY

ね? ただおなじ2変数でも、:- append_list( ?x, [1], ?y ). とかやると、答えが無限通りあって大変なことになるのでご注意を。

メモ

実は、append_list は双方向性が一番綺麗に決まる例であって、 一般にTyRuBaで書いた述語が全部ここまでうまくいくとは限りません。^^; 色々順番に注意しないと無限ループしてしまったり、などなど…。 その辺りの考え方はこの後の節で。

手続き型的な見方 (2003/06/24)

いきなり例が複雑になるのでご注意下さい。 append_list(A,B,?) みたいに、Rule に関数っぽい働きをさせられることがわかったので、 ここで「リストのソート」を書いてみます。目標は、 :- sort( [2,4,3,1], ?x ). とQueryを投げると ?x = [1,2,3,4] と返事が返ってくるようなもの。

//      注! このサンプルではリストに重複があるとソートに失敗します

// sort(A,B)
// リストAをソートするとBになる <= 
//   Aの並び替えがB and Bが順番に並んでいる
sort( ?before, ?after ) :- permute( ?before, ?after ), sorted( ?after ).

// permute(A,B)
// リストAの並び替えがB <=
//   空リストの並び替えは空リスト or
//   "Aの後ろ部分"の並び替えが"BからAの先頭要素を除いたもの"
permute( [], [] ).
permute( [?x|?xr], ?yr ) :- permute( ?xr, ?yr2 ), remove( ?x, ?yr, ?yr2 ).

// remove(a,A,B)
// リストAからaを除いたらB <=
//    Aの先頭がaな場合、BはAの後ろ部分 or
//    Aの先頭がa以外な場合、Bの後ろ部分からaを除く
remove( ?x, [?x|?xr], ?xr ).
remove( ?x, [?y|?xr], [?y|?yr] ) :- remove(?x,?xr,?yr).

// sorted(A)
// リストAは小さい順に並んでいる <=
//    Aは空リスト or
//    Aは要素が一個だけ or
//    先頭が2番目より小さくて、2番目以降は小さい順になっている
sorted( [] ).
sorted( [?] ).
sorted( [?1|[?2|?r]] ) :- less(?1, ?2), sorted( [?2|?r] ).

// less(A,B)
// AはBより小さい <=
//   AはBより1小さい or
//   AはあるCより1小さくて、CはBより小さい
// 
//   とりあえず、0~9の数にだけ正しく使えるものを暫定で。
//   多くの論理型言語にはこの意味の組み込みの述語があるので、
//   それを使えるなら使った方がベター
//   ※ TyRuBaの組み込み述語については後述
less1(0,1).
less1(1,2).
less1(2,3).
less1(3,4).
less1(4,5).
less1(5,6).
less1(6,7).
less1(7,8).
less1(8,9).
less(?x,?y) :- less1(?x,?y).
less(?x,?y) :- less1(?x,?z), less(?z,?y).

私はこのコードを上から順に書きました。つまり、sortというルールを書いたら permute と sorted が必要なことが判明したので、permuteを書いて、remove が要るので remove を書いて、sorted には less が要るので less を書いて…と。 ので、上から順にコメントを追っていただければ、 なんでこのコードになったか把握しやすいと思います。

>>> tyruba test.rub -silent -i
...略...
:- sort( [2,4,3,1], ?x ).
##QUERY : sort<[2,4,3,1],?x>

| ?x=[1,2,3,4] |
##END QUERY

-silent を付けておかないと、 探索状況が全部画面に表示されて邪魔なので付けることを推奨します。

順番

sortルールの permute と sorted を書く順を逆にして

sort( ?before, ?after ) :- sorted( ?after ), permute( ?before, ?after ).

としてみます。論理的には意味は前のプログラムと全く変わりません。 「Aの並び替えがBで、Bは昇順になっている」と言っていたのが 「Bは昇順になっていて、Aの並び替えがB」となっただけなので。 ところが、次のようなqueryを両方のプログラムに投げてみると、

:- sort( [9,8,7,6,5,4,3,2,1], ?x ).

答えが返ってくるまでの時間が全然違います。(permute, sorted の方は一瞬で 1,2,3,4,... が出るけれど、sorted, permute の方は非常に遅い。) 何故でしょう?答えは、TyRuBaが解を探索する順序にあります。

実行順序

:- sort( [9,8,7,6,5,4,3,2,1], ?x ).

というQueryを受けたとき処理系は何をするかを見ていきましょう。 まず、sort( ... ) を真にするような ?x を探すわけですから、 sort について書いてある rule を見に行きます。すると一個しかありません。

sort( ?before, ?after ) :- permute( ?before, ?after ), sorted( ?after ).

このruleを見ると、permute(...)かつsorted(...)ならsort(...)になるよ、 と書いてあります。今?beforeは[9,8,7,6,5,4,3,2,1]と決まっているので、あとは permute([9,8,7,6,5,4,3,2,1], ?after) と sorted(?after) が真になるような ?after を探せばよい、ということになります。じゃあまず permute(...) を真にするために、permute について述べているruleを探しましょう。

permute( [], [] ).
permute( [?x|?xr], ?yr ) :- permute( ?xr, ?yr2 ), remove( ?x, ?yr, ?yr2 ).

なんだか2つあるので両方見てみないと。一個目は permute([], []). は真、 と書いてあります。しかし今第一引数は[9,8,...,1]であって[]なので、 この事実は今回は関係ありません。仕方ないので次に二個目のRuleを調べます。 ?x = 9, ?xr = [8,7,6,...,1] と思うと、二個目のRuleの形にあてはまってます。 なので、このRuleの条件部分、permute(...)かつremove(...) を真にするにはどうすればいいか更に探索します。まず一つ目のpermute( ?xr, ?yr2 ) を真にしないといけないので………

まとめ

…という感じで、一番上、一番左から順に、条件を満たす物を探していくのが TyRuBa の動きかたなのでした。それで、解が見つかったらよし、 見つからなければ処理を戻して一個右や一個下を調べてみる「バックトラック」 を繰り返します。上の例だと、permute([],[]) に当てはまらなかったので 次に2個目を調べてみる…、って部分がバックトラックにあたります。 このように1ステップ戻るだけでなく、 場合によっては何ステップも前に探索が戻ることもありえます。で、 全パターン調べて見つからなかったら最後にFAILURE。

そう考えると、

sort( ?before, ?after ) :- permute( ?before, ?after ), sorted( ?after ).

は、「permuteかつsortedならばsort」という論理的な式と見るのではなく、 「sortというサブルーチンが呼ばれたら、 permuteサブルーチンを呼んで次にsortedサブルーチンを呼ぶ」と、 CやJavaなどの言語で関数を書いているのと同じように見ることもできてしまいます。 違いは、勝手に処理が行きつ戻りつする「バックトラック」があることだけ。

逆に言えば、Cなどと決定的に違うのはこの点なので、 これを活かすと「論理型的」なプログラムが書けるのではないかと思います。

例3 (2003/06/24)

「整数のリスト A を小さい順に並べ替えたら B になる」 という関係が成り立っているかどうかは、Aをどうやって並べ替えるのか… Aをバブルソートするかマージソートするのか、はたまた基数ソートするのか、 そういうことには関係のない話です。問題なのは手順ではなくて、 Aの内容とBの内容のみなので。

なのですが、上で見たように論理型言語の TyRuBa であっても、 やっぱり結果は実行の手順に依存します。従って、 ソートのアルゴリズムを指定したプログラムを書いてみることにも意味が出てきます。

実行順序が おなじみ QuickSort となるように sort ルールを書くと、次のようになるでしょう。

//      注! このサンプルではリストに重複があるとソートに失敗します

// qsort( AL, BL )
// ALをクイックソートするとBLになる
//    先頭要素を取り出して
//    それより小さいものと大きいものに分けて
//    それぞれをクイックソートして
//    連結
qsort( [], [] ).
qsort( [?1|?list], ?result ) :-
	split( ?1, ?list, ?list_smaller, ?list_larger ),
	qsort( ?list_smaller, ?sorted_smaller ),
	qsort( ?list_larger,  ?sorted_larger  ),
	append( ?sorted_smaller, [?1], ?tmp ),
	append( ?tmp, ?sorted_larger, ?result ).

// 前のappend_listと同じ。
append( [], ?list, ?list ).
append( [?1|?lst1], ?lst2, [?1|?lst3] ) :- append( ?lst1, ?lst2, ?lst3 ).

// split(a,BL,CL,DL)
// リストBLの中で、aより小さい物のリストがCL、aより大きい物のリストがDL
//   BLが空ならCLもDLも空 or
//   BLの先頭がa(?pivot)より小さかった時と大きかったときで場合分け
split( ?pivot, [], [], [] ).
split( ?pivot, [?x|?xr], [?x|?yr], ?zr ) :-
	less(?x,?pivot), split( ?pivot, ?xr, ?yr, ?zr ).
split( ?pivot, [?x|?xr], ?yr, [?x|?zr] ) :-
	less(?pivot,?x), split( ?pivot, ?xr, ?yr, ?zr ).

// 前のlessと同じ。
less1(0,1).
less1(1,2).
less1(2,3).
less1(3,4).
less1(4,5).
less1(5,6).
less1(6,7).
less1(7,8).
less1(8,9).
less(?x,?y) :- less1(?x,?y).
less(?x,?y) :- less1(?x,?z), less(?z,?y).
:- qsort( [5,2,1,6,8,3,9], ?x ).
##QUERY : qsort<[5,2,1,6,8,3,9],?x>

| ?x=[1,2,3,5,6,8,9] |
##END QUERY

or, ==, NOT, FINDALL, TEST, ... (2003/07/07)

or

条件を二つ並べるのではなく、";" で区切ることで、 どっちかが成り立てばOk、という条件を書く方法があります。 並べる方と;、どちらかわかりやすい方を 場合に応じて使い分けるとよいのではないかと。

father(Namihei, Sazae).
father(Namihei, Katsuo).
father(Namihei, Wakame).
mother(Fune, Sazae).
mother(Fune, Katsuo).
mother(Fune, Wakame).
father(Masuo, Ikura).
mother(Sazae, Ikura).
parent(?x, ?y) :- father(?x, ?y) ; mother(?x, ?y).
child(?x, ?y)  :- parent(?y, ?x).
:- parent(?x,?y).
##QUERY : parent<?x,?y>
..
| ?y=Katsuo ?x=Namihei |
| ?y=Wakame ?x=Namihei |
| ?y=Ikura ?x=Masuo |
| ?y=Sazae ?x=Namihei |.
| ?y=Katsuo ?x=Fune |
| ?y=Wakame ?x=Fune |
| ?y=Ikura ?x=Sazae |
| ?y=Sazae ?x=Fune |

∀, ∃, ...

Queryするときに、「条件を満たすか?」ではなく例えば 「条件を満たさないか?」のように、ちょっとひねった問い合わせができると便利です。 というわけで、幾つかの言語要素が用意されています。

まずNOT。「NamiheiはKatsuoの親じゃない」というQueryを送って失敗したり、 「NamiheiはIkuraの親じゃない」というQueryを送って成功したりしています。 NOTについては「全パターン試してみてダメだったらSUCCESS」 という動作をしているだけなので、変数を使って問い合わせてみても 「Queryが失敗するような割り当てを探して返す」 みたいな動作はしてくれないので注意が必要です。

:- NOT(parent(Namihei, Katsuo)).
##QUERY : NOT(parent<Namihei,Katsuo>)
.o
FAILURE
##END QUERY
:- NOT(parent(Namihei, Ikura)).
##QUERY : NOT(parent<Namihei,Ikura>)
.oo
| SUCCESS |
##END QUERY

次にFINDALL。Funeの子供である?xを全部探してきて、 それを?childlistという変数に入れて、それが要素数3のリストかどうかチェック。 答えを一度に全部見つけてリストにして変数に入れる、 というのがこのFINDALLです。

is_3( [?,?,?] ).
** assertion added to rulebase **
:- FINDALL(child(?x, Fune), ?x, ?childlist), is_3(?childlist).
##QUERY : FINDALL(child<?x,Fune>,?x,?childlist),is_3<?childlist>
.....
| ?childlist=[Katsuo,Wakame,Sazae] |
##END QUERY

FIRST。Katsuoの親はNamiheiとFuneの二人ですが、 最初の一個の答えだけを返して他は無視するようにするもの。 どれか一個でも条件を満たす答えが欲しいので最初の一個で十分、 というときの処理の高速化などに使えます。

:- FIRST(parent(?p, Katsuo)).
##QUERY : FIRST(parent<?p,Katsuo>)
..
| ?p=Namihei |
##END QUERY

お次はUNIQUE。条件を満たす答えが唯1つ、0個でもなく2個でもない、 というときだけSUCCESSしてその答えを返す指令です。

:- UNIQUE(child(?x, Namihei)).
##QUERY : UNIQUE(child<?x,Namihei>)
.oo
FAILURE
##END QUERY
:- UNIQUE(child(?x, Sazae)).
##QUERY : UNIQUE(child<?x,Sazae>)
..o.
| ?x=Ikura |
##END QUERY

TEST。TEST()の中で使った変数(下の例なら?x、です) に何が割り当てられたかとかは忘れて、ただSUCCESSかFAILUREか、 の答えのみを返すようにするのがこれです。使いどころがイマイチ不明。

:- TEST(child(?x, Namihei), parent(?x,Ikura)).
##QUERY : TEST(child<?x,Namihei>,parent<?x,Ikura>)
H.oooooooo
| SUCCESS |
##END QUERY

BOUND, ==

二つが同じものかを調べるには

is_same(?x,?x).

というルールを用意しておけば事足りるのですが、 特別に == というのも用意されています。

:- Abc == Abc .
##QUERY : Abc==Abc

| SUCCESS |
##END QUERY

ただしこの==では、Abc==?xと書いたら変数?xを定めてくれる、 というようなことをしてくれないので注意。片側が変数で片側がそうでない場合は、 FAILします。ので、特別な用途でない限り上のis_sameの方が無難。

最後に特殊なものとして、BOUND。変数を渡すと、 その変数がすでに束縛済みかどうか?によってSUCCESS/FAILUREします。 下の例では、何もしていない変数?xを渡すとBOUNDがFAILして、 is_sameによって?x=Hoge、と決めてしまった変数?xを渡すと SUCCESS しているのが見て取れると思います。

:- BOUND(?x).
##QUERY : BOUND(?x)

FAILURE
##END QUERY
:- is_same(?x, Hoge), BOUND(?x).
##QUERY : is_same<?x,Hoge>,BOUND(?x)
.
| ?x=Hoge |
##END QUERY

例4:高階述語 (2003/07/14)

FINDALLを使うと有効な例を書いてみます。 「Namiheiの子供の中で一番年上なのは誰?」

// Sazaeさん一家のfact定義
father(Namihei, Sazae).
father(Namihei, Katsuo).
father(Namihei, Wakame).
// ... 略 ...

parent(?x, ?y) :- father(?x, ?y) ; mother(?x, ?y).
child(?x, ?y)  :- parent(?y, ?x).
older(Sazae, Katsuo).
older(Katsuo, Wakame).
older(Sazae, Wakame).
// ... 略 ...

// ソート用ルールの定義
qsort( [], [], ? ).
qsort( [?1|?list], ?result, ?less ) :-
	split( ?1, ?list, ?list_smaller, ?list_larger, ?less ),
	qsort( ?list_smaller, ?sorted_smaller, ?less ),
	qsort( ?list_larger,  ?sorted_larger, ?less  ),
	append( ?sorted_smaller, [?1], ?tmp ),
	append( ?tmp, ?sorted_larger, ?result ).

append( [], ?list, ?list ).
append( [?1|?lst1], ?lst2, [?1|?lst3] ) :- append( ?lst1, ?lst2, ?lst3 ).

split( ?pivot, [], [], [], ? ).
split( ?pivot, [?x|?xr], [?x|?yr], ?zr, ?less ) :-
	?less(?x,?pivot), split( ?pivot, ?xr, ?yr, ?zr, ?less ).
split( ?pivot, [?x|?xr], ?yr, [?x|?zr], ?less ) :-
	?less(?pivot,?x), split( ?pivot, ?xr, ?yr, ?zr, ?less ).

と準備をした後で、「FINDALLで子供を全員検索してきて、 それを年の順でソートした結果の先頭を取り出し」、という手順で行きましょう。 こんな感じ。

:- FINDALL( child(?x, Namihei), ?x, ?childlist ),
   qsort( ?childlist, [?oldest|?], older ).
##QUERY : FINDALL(child<?x,Namihei>,?x,?childlist),
                  qsort<?childlist,[?oldest|?],older>
.......oooo.ooH..oHooHHooHHooHoooHH
| ?oldest=Sazae ?childlist=[Katsuo,Wakame,Sazae] |o
##END QUERY

この例の面白い点は、FINDALLよりもむしろqsortの定義。 上の方で書いたqsortと比べて、 一つ引数が増えて比較用の述語を受け取るようになっています。 最終的にsplitの定義内で使われています。qsortを使うときは、 older や less などの述語名を書いておけばOk。

TyNI (2003/07/14)

上の方で書いた less 述語の例が物凄く無駄っぽかったことからもおわかりのように、 TyRuBa内部の「論理」だけで普通の整数演算などを全て定義するというのは (不可能ではないのですが)あまり良い方法とは言えそうにありません。 そこで、Javaで定義された関数と連携するための TyRuBa Native Interface、 略して TyNI と呼ばれる機構が存在します。

JAVASEND

とりあえず足し算になるような述語を書いてみます。 次のような足し算関数を定義して、javaコンパイラでコンパイル。 SampleTyni.class が生成されます。

// SampleTyni.java -- generates SampleTyni.class
public class SampleTyni
{
	public static int add( Integer a, Integer b )
	{
		return a.intValue() + b.intValue();
	}
}

で、これを呼び出すには次のようなTyRuBaのルールを書きます

add(?a, ?b, ?c) :- 
	JAVASEND( static,
	          [add, #SampleTyni, #java.lang.Integer, #java.lang.Integer],
	          [?a,?b], ?c ).

クラスメソッド呼び出しであって(static)、そのメソッドの名前はaddで、 クラス名はSampleTyni引数の型はIntegerとInteger、実際の引数は?aと?bで、 返値は?cに入れる、という指定をしてJavaのメソッドへSendする、 ってな形になっています。ではLet's Go!

:- add(1,2,?x).
##QUERY : add<1,2,?x>
.
| ?x=3 |
##END QUERY

論理型風に

上で定義した add関数 だけでは、当たり前ながら :- add(?x, 3, 4). と問い合わせると ?x = 1 が返る、 という動作はしてくれません。しかしこれでは寂しすぎるので、 もう少し変更を加えましょう。

public class SampleTyni {
	public static int add( Integer a, Integer b ) {
		return a.intValue() + b.intValue();
	}
	public static int sub( Integer a, Integer b ) {
		return a.intValue() - b.intValue();
	}
}
add(?a, ?b, ?c) :- 
	integer(?a), integer(?b), JAVASEND( static,
	    [add, #SampleTyni, #java.lang.Integer, #java.lang.Integer],
	    [?a,?b], ?c ) |
	integer(?b), integer(?c), JAVASEND( static,
	    [sub, #SampleTyni, #java.lang.Integer, #java.lang.Integer],
	    [?c,?b], ?a ) |
	integer(?a), integer(?c), JAVASEND( static,
	    [sub, #SampleTyni, #java.lang.Integer, #java.lang.Integer],
	    [?c,?a], ?b ) .

TyRuBaの方のintegerという述語は見慣れませんが、項が整数であるか? を判定するために用意された述語です。(java.lang.Class.isInstance を使ってTyNIで実装されていますので、調べてみると面白いかも。) また、縦棒 | も初出ですが、これは"排他的なOR"を意味します。 ORの場合は一個解が見つかっても他でも見つからないか探しに行きますが、 | の場合は一個見つかれば他は見に行きません。

:- add(1,?x,9).
##QUERY : add<1,?x,9>
...ooHHoo
| ?x=8 |
##END QUERY

というわけでこんなことも可能な述語が書けました。でももう一歩足りない。

JAVASOLVE

:- add(?x,?y,4). と書いたら足して 4 になるペアを片っ端から探してきて欲しいよね、ということで。 もっとも、ホントに足して4になる数を全部もってくると 20億組くらいはありそうなので、ここでは正の数に限ることにします。

import java.util.*;

public class SampleTyni {
	public static int add( Integer a, Integer b ) {
		return a.intValue() + b.intValue();
	}
	public static int sub( Integer a, Integer b ) {
		return a.intValue() - b.intValue();
	}
	public static Vector add_find( Integer a ) {
		final int av = a.intValue();
		Vector result = new Vector();
		for(int i=0; i<av; ++i) {
			Integer[] r = new Integer[2];
			r[0] = new Integer(i);
			r[1] = new Integer(av-i);
			result.add( r );
		}
		return result;
	}
}
add(?a, ?b, ?c) :- 
	integer(?a), integer(?b), JAVASEND( static,
	    [add, #SampleTyni, #java.lang.Integer, #java.lang.Integer],
	    [?a,?b], ?c ) |
	integer(?b), integer(?c), JAVASEND( static,
	    [sub, #SampleTyni, #java.lang.Integer, #java.lang.Integer],
	    [?c,?b], ?a ) |
	integer(?a), integer(?c), JAVASEND( static,
	    [sub, #SampleTyni, #java.lang.Integer, #java.lang.Integer],
	    [?c,?a], ?b ) |
	integer(?c), JAVASOLVE( static,
	    [add_find, #SampleTyni, #java.lang.Integer],
	    [?c], [?a,?b] ) .

複数の答えがあるような述語を定義する場合解のVectorを返す、 というのが今までと異なる点です。あと、解として二つの値 (この場合の?xと?yのように)を返す場合は、配列にして返します。 JAVASOLVEの構文はJAVASENDとだいたい同じ。

:- add(?x,?y,4).
##QUERY : add<?x,?y,4>
...HHoo
| ?y=4 ?x=0 |
| ?y=3 ?x=1 |
| ?y=2 ?x=2 |
| ?y=1 ?x=3 |
##END QUERY

めでたしめでたし。

JAVASEND, JAVASOLVE まとめ

// true/falseを返すメソッドの場合
JAVASEND( static,
          [メソッド名, #クラス名, #引数クラス名1, #引数クラス名2, ...],
          [引数1, 引数2, ...] ).
// 値を返す場合
JAVASEND( static,
          [メソッド名, #クラス名, #引数クラス名1, #引数クラス名2, ...],
          [引数1, 引数2, ...],
          返値 ).
// インスタンスメソッドの時(booleanを返すなら返値省略可)
JAVASEND( インスタンス,
          [メソッド名, #クラス名, #引数クラス名1, #引数クラス名2, ...],
          [引数1, 引数2, ...],
          返値 ).

SUCCESS, FAILURE の判定をおこなってtrue/falseを返すだけのメソッドの時は、 返値を省略してそのままJAVASEND全体を成功/失敗させることができます。 値を複数返したいときは配列を返して、受け取り側で [?1,?2,..] みたいに分割して受け取ります。

解が複数ある可能性があるときは、JAVASENDの代わりにJAVASOLVE(構文は全く同じ) を使います。Java側では解のVectorを返すようにすると、あとは TyRuBa 側で勝手に探索ループを回してくれます。

ただ、全部TyRuBaで書いた述語と違って、 JAVASOLVEでは最初に全部の解を生成してしまうため、 無限個解がある問題をうまく扱えないなどの弱点があります。 この辺りはリファレンスに It may not be particularly useful for more complicated things (yet :-) と書かれていたりするので、 今後に期待(^^;

例5:ライブラリ (2003/07/15)

TyNIの使用例として、TyRuBa のライブラリで実際に定義されている述語を見てみることにしました。 これらはユーザープログラムから断り無くruleやqueryに使うことが可能です。 配布パッケージ内の src/lib/*.rub や src/tyRuBa/engine/Tyni.java が該当するソースになります。

以下はTyRuBaのライブラリソースからの引用です。

// tyni.rub
sum(?a,?b,?c) :-
  integer(?a),integer(?b),
    JAVASEND(static,
           [add,#tyRuBa.engine.Tyni,#java.lang.Integer,#java.lang.Integer],
           [?a,?b],?c) | 
  integer(?a),integer(?c),
    JAVASEND(static,
           [subtract,#tyRuBa.engine.Tyni,#java.lang.Integer,#java.lang.Integer],
           [?c,?a],?b) |
  integer(?b),integer(?c),
    JAVASEND(static,
           [subtract,#tyRuBa.engine.Tyni,#java.lang.Integer,#java.lang.Integer],
           [?c,?b],?a) .

整数の足し算。さっき書いたのとだいたいおんなじ。

// tyni.rub
greater(?x,?y) :- integer(?x),integer(?y),
  JAVASEND(static,
        [greater,#tyRuBa.engine.Tyni,#java.lang.Integer,#java.lang.Integer],
        [?x,?y]).

大小比較も用意されています。なので、上の方の sort の例のソースでは自前のlessよりこのgreaterを使った方が実際上はベター。

// string.rub
string_append(?s1,?s2,?s12) :- 
    string(?s1),string(?s2),
      JAVASEND(?s1,
               [concat,#java.lang.String,#java.lang.String],[?s2],?s12) |
    string(?s12),
      JAVASOLVE(static,
                [string_split,#tyRuBa.engine.Tyni,#java.lang.String],
                [?s12],[?s1,?s2]).

"mojiretsu" などと書くことで TyRuBa では文字列も使えるのですが、 その連結(もしくは双方向性から、分離)を行う述語の定義です。 staticでないメソッドを使っている例になっています。

// string.rub
to_upper_case(?string,?STRING) :- 
   string(?string),
     JAVASEND(?string,[toUpperCase,#java.lang.String],[],?STRING).

文字列の例をもう一つ。

// mail.rub
send_email(?to,?from,?smtp_host,?subject,?msgText) :-
  debug_print({** sending email to: ?to}),
    JAVASEND(static,[sendMessage,#tyRuBa.goodies.mail.SendMessage,
             #java.lang.String,#java.lang.String,#java.lang.String,
             #java.lang.String,#java.lang.String],
             [?to,?from,?smtp_host,?subject,?msgText]).

debug_printはまた別のところで定義されてるものですがここでは置いときます。 メールを送る述語、と、こうなってくるともう完璧に手続き型言語の世界ですね。 ソケットの開閉などから全部TyRuBaの述語に落としてSMTP処理をTyRuBaで書く、 ということも一応可能ですが、そんな手間をかけるよりは Java でやった方が簡単な部分は Java に回しましょう、といった感じでしょうか。 適材適所。

その他のライブラリ述語

最後に、TyNIを使わない定義済みの述語で便利なモノを幾つか紹介。 src/lib/prolog.rub からの引用です。

// リストの要素かどうか?
member(?E,[?E | ?R]).
member(?E,[?A | ?R]) :- member(?E,?R).

// 二つのリストを、一つの"リストのリスト"にまとめます
zip([],[],[]).
zip([?a|?as],[?x|?xs],[[?a,?x]|?axs]) :- zip(?as,?xs,?axs). 

// リストと、述語と、要素毎の適用結果リスト
//   map<a>(b,c) という書き方は map(a,b,c) とほぼ同じ意味ですが、
//   こちらは map<a> の部分で切れるので述語の"部分適用"が可能になります
map<?f>([],[]).
map<?f>([?x|?xs],[?fx|?fxs]) :- 
   ?f(?x,?fx), map<?f>(?xs,?fxs).

// 「1大きい」という述語から「以上」という述語を作りたいとき、
// 「子供である」という述語から「子孫である」という述語を作りたいとき。
// 「?r」という述語から「?rという関係を何回か繰り返した関係である」と
// いう述語を作りたいときに、この closure<?r> を使うと吉です。
closure<?r>(?a,?b) :- equal(?a,?b).
closure<?r>(?a,?c) :- BOUND(?a),
                      ?r(?a,?b),
                      closure<?r>(?b,?c).
closure<?r>(?a,?c) :- NOT(BOUND(?a)),
                      ?r(?b,?c),
                      closure<?r>(?a,?b).

その他のトピック (2003/07/19)

TyRuBaに備わっている言語的な特徴をもう幾つかご紹介。

tabling

最初と二番目が1,残りは直前とそのまた前の和、という、いわゆるフィボナッチ数列 {1,1,2,3,5,8,13,21,34,...} の第 N 番目を計算するプログラムを書いてみます。 まずはC言語で、定義に忠実に書いてみます。

int fib(int n) {
	if(n==0 || n==1)
		return 1;
	return fib(n-1) + fib(n-2);
}

これを使って(オーバーフローのことはここでは忘れて)計算してみると、 10や20番目ならいいのですが、50番目、100番目となると 時間がかかりすぎて終わらない、ということに気づかれるかと思います。 それもそのはずで、N番目を求めるのに上の関数定義では、fibが 2 の N 乗回という莫大な回数呼び出されてしまいますから。 マジメにフィボナッチ数を計算するなら、 先頭から順にループで求めていくような方法に変えなくてはいけません。

同じことをTyRuBaで書いてみましょう。

fib(?n, ?fibn) :-
	equal(?n,0), equal(?fibn,1) |
	equal(?n,1), equal(?fibn,1) |
	sum(?n1,1,?n), fib(?n1,?fibn1),
	sum(?n2,2,?n), fib(?n2,?fibn2),
	sum(?fibn1,?fibn2,?fibn).
:- fib(50, ?x).
##QUERY : fib<50,?x>
.oooHHHooooooHHHooooooHHHooooooHHHooooooHHHooooooHHHooooooHHHooooooHHHooooooHHHo
oooooHHHooooooHHHooooooHHHooooooHHHooooooHHHooooooHHHooooooHHHooooooHHHHooooHHHo
oooooHHHooooooHHHooooooHHHooooooHHHooooooHHHooooooHHHooooooHHHooooooHHHooooooHHH
ooooooHHHooooooHHHooooooHHHHoHHHHHHHoHHHHHooooooHHHHHoooHoHHHHHoooHoHHHHHoooHoHH
HHHoooHoHHHHHoooHoHHHHHoooHoHHHHHoooHoHHHHHoooHoHHHHHoooHoHHHHHoooHoHHHHHoooHoHH
HHHoooHoHHHHHoooHoHHHHHoooHoHHHHHoooHoHHHHHoooHoHHHHHoooHoHHHHHoooHoHHHHHoooHoHH
HHHoooHoHHHooHoooHoHHHooHoooHoHHHooooooHHHHHHHHoooHoHHHooooooHHHHHHHHoooHoHHHooo
oooHHHHHHHHoooHoHHHooooooHHHHHHHHoooHoHHHooooooHHHHHHHHoooHoHHHooooooHHHHHHHHooo
HoHHHooooooHHHHHHHHoooH
| ?x=-1109825406 |
##END QUERY

こちらは結構すぐに処理が返ってきました。TyRuBaでは過去の Query の答えを記憶しておいて、次おなじ問い合わせが来たら即座に同じ答えを返す、 という最適化が行われているので、fibの呼び出しがおよそ50回で済んでいます。

複数答えがある例だと勝手に前の答えを返されると困りますが、 その辺りはうまいこと処理されている模様。

regexp

TyNIによって正規表現マッチングが提供されています。

:- re_match( /1+2*3+/, "113" ).
##QUERY : re_match<org.apache.regexp.RE@3eca90,113>
.
| SUCCESS |
##END QUERY
:- re_match( /1+2*3+/, "223" ).
##QUERY : re_match<org.apache.regexp.RE@3eca90,223>
.
FAILURE
##END QUERY

開発された時期の関係でしょうか、java.util.regex でなく apache.org の正規表現ライブラリによって実装されているみたいです。

Javaのコード生成 (2003/07/20)

付属の javagenerator ライブラリによって、Javaのソースを生成することができます。 つまり、TyRuBaによってJavaのメタプログラミングを行おう、ということですね。

// sample.rub
class_(MyClass).
  var_(MyClass,int,hoge).
  var_(MyClass,String,fuga).
  method_(MyClass,int,addHoge,[int],[x],{
    return hoge + x;}).

こんなソースを書いて…generate述語で問い合わせると

>> tyruba sample.rub -i
...
:- generate(MyClass, ?result).
##QUERY : generate<MyClass,?result>
.......................H.HH....ooo.oooooo.......H.HHHH.HHHHHHH..H...oooooooooHHo
HHooooooooooHHoHHHH...HHH.HHHHH
| ?result={/*****************************************************************\
 * File:        MyClass.java
 * Author:      TyRuBa
 * Meta author: Kris De Volder <kdvolder@cs.ubc.ca>
\*****************************************************************/
package ;


 class MyClass  {

    int addHoge(int x) {
    return hoge + x;
    }
    String fuga;
    int hoge;
}
} |
##END QUERY

とまあこんな感じにJavaのコードが返ってきます。package 部分が空になっちゃってますが、これは class_(net.kmonos.MyClass) のようにクラス名を完全に修飾しておけば適宜埋めてくれます。 見慣れないのが method_ に渡している { ... } ですが、 この中カッコでは括弧の対応がちゃんと取れている任意の文字列 をくくることができます。{}の中に ?... と変数名が入っていると 変数に束縛された値に自動で置き換えられますので、 これを使ってJavaのコードのテンプレートを書いていくことになります。

class_(net.kmonos.MyClass).
  implements_(net.kmonos.MyClass, Serializable).

  var_(net.kmonos.MyClass,int,data,100).
    private_( var<net.kmonos.MyClass,data> ).
    static_( var<net.kmonos.MyClass,data> ).

  constructor_(net.kmonos.MyClass,[int,int],[x,y],{
    data = data * x  + y;}).

:- generate(net.kmonos.MyClass, ?result).
package net.kmonos;

 class MyClass implements Serializable  {

    MyClass(int x,int y) {
    data = data * x  + y;
    }
    static private int data=100;
}

また全く意味のないサンプルですが、こんな感じに、Java のクラス定義に必要な言語要素はだいたい述語として実装されています。 詳しくはTyRuBaの配布書庫内の src/lib/javagenerator.rub をチェック。 一見マジカルな述語 generate も、簡潔な TyRuBa のプログラムとして記述されているので面白いです。

少し実用的な例

この機能、どんな風に使うと便利でしょう?次のようなルールを考えてみます。

// read_only変数なら、変数である
var_( ?Class,?type,?name )
  :- read_only_var( ?Class,?type,?name ).

// read_only変数なら、privateである
private_( var<?Class,?name> )
  :- read_only_var( ?Class,?type,?name ).

// read_only変数なら、getter関数を持つ
getter_func( ?Class, ?type, ?getName, ?name )
  :- read_only_var( ?Class,?type,?name ),
     capitalize( ?name, ?Name ), string_append(get,?Name,?getName).

// getter関数とは、かくかくしかじかなメソッドである
method_( ?Class, ?type, ?getName, [], [], {return ?name;})
  :- getter_func( ?Class, ?type, ?getName, ?name ).

// getter関数は、publicである
public_( method<?Class,?getName,[]> )
  :- getter_func( ?Class, ?type, ?getName, ?name ).

こんな風に使います。

class_(net.kmonos.MyClass).
  read_only_var( net.kmonos.MyClass, int, data ).
:- generate(net.kmonos.MyClass, ?result).
 class MyClass  {

    public int getData() { return data;
    }
    private int data;
}

read_only とだけ指定すれば、変数/getter 関数の宣言・定義を全部自動生成してくれるようになりました。 こんな風に、定義に関する抽象的な概念(例えば"読み取り専用") から具体的な概念(例えば"privateでgetter関数があって...") に落とすルールをTyRuBaで記述して、人間が書くクラス定義をより抽象化できる、と。

read_only くらいだとあまりありがたみが湧きませんが(^^;、 公式ページ のdesign_pattern_example内の「VisitorパターンのAcceptor-Visitorの関係にある」 ようなクラス群を生成するサンプルなど、 大がかりなものになってくると結構TyRuBaが活きてきます。

read_onlyの例のように、こういう概念は「これこれはこんなメソッドを持っている」 とか「これこれはこのインターフェイスを実装する」とか、 非常に論理型言語に向いた形で定義されるので、generator 言語としては 確かにTyRuBaは向いているかも、と思わされました。

まとめ

…以上、TyRuBa に関しての記事はここまで。

前半は主に「論理型言語とは何ぞや?」に絞って書いてみました。 後半は「Javaとの連携」について。個人的には、"TyNI" という形で論理の中と外が綺麗に読みやすく区分けされるのに感銘をうけました。 悪く言ってしまえば教科書的なのですが、一般に使われている Prolog 等の処理系と比べて、ライブラリの記述など圧倒的にわかりやすく思いました。 この辺りはJavaがReflectionという強力な機構を持っているのが大きい、 というのもあるのかな、とか。

ではではー。

presented by k.inaba   under NYSDL.