PrologCafeを使ってJava言語でProlog言語で書かれた項の文字列をパースするの巻


## はじめに

[PrologCafe](http://kaminari.istc.kobe-u.ac.jp/PrologCafe/)とはJava言語で実装されたProlog言語の処理系です。PrologCafeではPrologの項を`Term`クラスによって表現しています。しかし、複雑な項を`Term`クラスで組み立てるには骨が折れます。

例えば以下の項を`Term`クラスで表現したいとします。

```prolog:節
neko :- write(meow), nl
```

これは普通、節と呼ばれますが、別の書き方をすれば以下のようになります。つまり、節は`':-'/2`や`','/2`といった複合項を組み合わせて表現されています。ちなみに本体部の長い節では`','/2`をネストさせて表現しています。

```prolog:項による節の表現
':-'(neko, ','(write(meow), nl))
```

すこし脱線しますが、SWI-Prologで試してみましょう。

```bash:SWI-Prologを使った例
$ swipl --quiet
?- assert((neko :- write(meow), nl)).
true.

?- assert(':-'(neko , ','(write(meow), nl))).
true.

?- listing.

:- thread_local thread_message_hook/3.
:- dynamic thread_message_hook/3.
:- volatile thread_message_hook/3.


:- dynamic neko/0.

neko :-
	write(meow),
	nl.
neko :-
	write(meow),
	nl.
true.
```

異なる記法で`assert/1`された項が`listing`してみると同じであることが確認できます。

話を戻して、PrologCafeでこの節を組み立ててみましょう。

```java:節の構築
package maglog;

import jp.ac.kobe_u.cs.prolog.lang.StructureTerm;
import jp.ac.kobe_u.cs.prolog.lang.SymbolTerm;
import jp.ac.kobe_u.cs.prolog.lang.Term;

public class Example {

	// 「neko :- write(meow), nl」の項による表現
	public static Term makeNeko() throws Exception {
		// nl
		SymbolTerm nl = SymbolTerm.makeSymbol("nl", 0);
		SymbolTerm write_nl = nl;
		// write(meow)
		SymbolTerm meow = SymbolTerm.makeSymbol("meow", 0);
		SymbolTerm write = SymbolTerm.makeSymbol("write", 1);
		StructureTerm write_meow = new StructureTerm(write, new Term[] { meow });
		// ','(write(meow), nl)
		SymbolTerm comma = SymbolTerm.makeSymbol(",", 2);
		StructureTerm body = new StructureTerm(comma, new Term[] { write_meow, write_nl });
		// neko
		SymbolTerm neko = SymbolTerm.makeSymbol("neko", 0);
		SymbolTerm head = neko;
		// ':-'(neko, ','(write(meow), nl))
		SymbolTerm tonbo = SymbolTerm.makeSymbol(":-", 2);
		StructureTerm clause = new StructureTerm(tonbo, new Term[] { head, body });
		return clause;
	}

}
```

かなりしんどいです。さすがにこれはProlog言語で書きたい場合もあるでしょう。というわけで、今回はJava言語の`String`で記述されたPrologの節を`Term`クラスに変換してみようというメモの巻です。

## 設計

Prologでは、`read/1`や`read/2`を使えば入力ストリームをパースして項として読み込むことができます。以下にSWI-Prologでの`read/1`の例を示します。

```linenums:bash:linenums:SWI-Prologでの例
$ swipl --quiet
?- read(Term), assert(Term).
|: neko :- write(meow), nl.
Term = (neko:-write(meow), nl).

?- listing.

:- thread_local thread_message_hook/3.
:- dynamic thread_message_hook/3.
:- volatile thread_message_hook/3.


:- dynamic neko/0.

neko :-
	write(meow),
	nl.
true.

?- neko.
meow
true.
```

簡単に動作をメモしておくと、まず、`read(Term)`がコールされるとプロンプトが`?-`ではなく`|:`になり標準入力からの入力を受け付けます。ここでキーボードなどから`neko :- write(meow), nl.`を入力すると、入力された文字列がパースされ、項となり、`assert(Term)`によって節データベースへ格納されます。次に、`listing/1`で節データベースの内容を確認しています。最後に、節データベースに格納された`neko/1`をコールしています。

したがって、PrologCafeを使ってJava言語で`read/1`や`read/2`を使えば書けば、`String`をラップした`StringReader`から`Term`へ変換できそうです。

## 実装

先日のメモ「[PrologCafeでProlog実行エンジンをシングルスレッドで動かすの巻](http://qiita.com/61503891/items/ce883118325a4fe73c68)」で作成した`PrologContext`クラスを利用します。

手順は以下のとおりです。

1. 項の文字列を`StringReader`に食わせる。
2. 上記`StringReader`を`read/2`の入力ストリームに結びつける。（`read/2`は`read/1`と違って任意のストリームを結びつけることができます。）
3. [PrologCafeの`read/2`は読み込みに失敗した場合、カレント出力ストリームにエラーメッセージを書き出す](https://github.com/mh61503891/plcafe/blob/master/src/builtin/builtins.pl#L1059)ので、`set_output/1`で`StringWriter`をカレント出力ストリームに結びつけてエラーメッセージを取得できるようにする。
4. Prologエンジンをグルグルまわすと`String`が`Term`に変換される。

```PrologUtil.java
package maglog.prolog;

import java.io.PrintWriter;
import java.io.PushbackReader;
import java.io.StringReader;
import java.io.StringWriter;
import jp.ac.kobe_u.cs.prolog.builtin.PRED_read_2;
import jp.ac.kobe_u.cs.prolog.builtin.PRED_set_output_1;
import jp.ac.kobe_u.cs.prolog.lang.JavaObjectTerm;
import jp.ac.kobe_u.cs.prolog.lang.Predicate;
import jp.ac.kobe_u.cs.prolog.lang.Success;
import jp.ac.kobe_u.cs.prolog.lang.Term;
import jp.ac.kobe_u.cs.prolog.lang.VariableTerm;

/**
 * @author Masayuki Higashino
 */
public class PrologUtil {

	public static Term read(String s) throws Exception {
		PrologContext context = new PrologContext();
		context.init();

		StringReader reader = new StringReader(s);
		StringWriter writer = new StringWriter();

		Term input = new JavaObjectTerm(new PushbackReader(reader));
		Term output = new JavaObjectTerm(new PrintWriter(writer));
		Term term = new VariableTerm(context.getEngine());

		Predicate p1 = new Success(context);
		Predicate p2 = new PRED_read_2();
		p2.setArgument(new Term[] { input, term }, p1);
		Predicate p3 = new PRED_set_output_1();
		p3.setArgument(new Term[] { output }, p2);
		context.setPredicate(p3);

		while (!context.isStopped())
			context.execute();

		if (context.getResult() == PrologContext.Result.FAILURE)
			throw new Exception(writer.toString());
		return term;
	}

}
```

## 実験

では実際に動かしてみましょう。

```Example.java
package maglog;

import maglog.prolog.PrologContext;
import maglog.prolog.PrologUtil;
import jp.ac.kobe_u.cs.prolog.lang.StructureTerm;
import jp.ac.kobe_u.cs.prolog.lang.SymbolTerm;
import jp.ac.kobe_u.cs.prolog.lang.Term;

public class Example {

	// 「neko :- write(meow), nl」の項による表現
	public static Term makeNeko() throws Exception {
		// nl
		SymbolTerm nl = SymbolTerm.makeSymbol("nl", 0);
		SymbolTerm write_nl = nl;
		// write(meow)
		SymbolTerm meow = SymbolTerm.makeSymbol("meow", 0);
		SymbolTerm write = SymbolTerm.makeSymbol("write", 1);
		StructureTerm write_meow = new StructureTerm(write, new Term[] { meow });
		// ','(write(meow), nl)
		SymbolTerm comma = SymbolTerm.makeSymbol(",", 2);
		StructureTerm body = new StructureTerm(comma, new Term[] { write_meow, write_nl });
		// neko
		SymbolTerm neko = SymbolTerm.makeSymbol("neko", 0);
		SymbolTerm head = neko;
		// ':-'(neko, ','(write(meow), nl))
		SymbolTerm tonbo = SymbolTerm.makeSymbol(":-", 2);
		StructureTerm clause = new StructureTerm(tonbo, new Term[] { head, body });
		return clause;
	}

	public static void main(String[] args) {
		try {
			Term t1 = Example.makeNeko();
			Term t2 = PrologUtil.read("neko :- write(meow), nl.");

			System.err.println(t2);
			System.err.println(t1);

			if (t2.equals(t1)) {
				System.err.println("equals: OK");
			} else {
				System.err.println("equals: NG");
			}

			PrologContext c = new PrologContext();
			c.init();
			if (t2.unify(t1, c.getEngine().trail)) {
				System.err.println("unify: OK");
			} else {
				System.err.println("unify: NG");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

```

```bash:出力
:-(neko,,(write(meow),nl))
:-(neko,,(write(meow),nl))
equals: OK
unify: OK
```

Java言語での`equals`とProlog言語での`unify`も成功していますね。`PrologUtil.read("neko :- write(meow), nl.")`とすれば`String`が`Term`に変換されるわけです。よしよし。（テストはまた別の機会に。）

## おわりに

ニッチすぎてストックされる気がしない。