知識をクライアントに送付するprologサーバー

prologの二分木化された日本語wikipediaの出力は、TCPサーバーでやろうとしているわけだが、そのためにはprologでサーバーを立ち上げなければならない。クライアントとのマルチバイト文字のやりとりで手間取ったが、それもなんとかクリアしたので、現状をここにアップしておく。

まず、prologのTCPサーバーは、swi-prologのマニュアルにあるものを参考に以下のように作成する。

%%%%
%% 知識データセクション、簡単な例
%%%%
animal(ライオン,肉食動物).
animal(キリン,草食動物).

%%%%
%% TCPサーバーセクション
%% 参照:
%% http://www.swi-prolog.org/pldoc/man?section=stream-pools
%%%%

%% home brewでインストールしたswi-prologでは、次のライブラリが読めない可能性がある
%% その場合は、ソースからコンパイルし直し、最新バージョンを入れる
:- use_module(library(streampool)).

server(Port) :-
    %% tcpソケットの作成
    tcp_socket(Socket),
    %% ソケットをアドレスにつなげる 第二引数は、portのみか、HostPort
    tcp_bind(Socket, Port),
    %% ソケットからリクエストを受け取る、第二引数は、ペンディングリクエストの上限
    tcp_listen(Socket, 5),
    %% ソケットとコミュニケーションのためのストリームを作成する
    tcp_open_socket(Socket, In, _Out),
    %% Inが利用可能になったら、accept(Socket)が呼び出される
    add_stream_to_pool(In, accept(Socket)), 
    %% loopが空になるまでdispatch_stream_poolが呼び出される
    %% dispatch_stream_poolは入力があるとadd_stream_to_poolのGoalを呼び出す
    stream_pool_main_loop.

accept(Socket) :-
    %% ソケットからのクライアントからのリクエストを待つ
    tcp_accept(Socket, Slave, Peer),
    tcp_open_socket(Slave, In, Out), 
    add_stream_to_pool(In, client(In, Out, Peer)).

client(In, Out, _Peer) :-
    %% 入力ストリームから次の行を読み出す 結果はCommandにユニファイされる
    %% 改行までか、ファイルの終わりまで読み込まれる 改行コードは削除される
    %% 改行を含むブロックを読み込むときは read_line_to_codes/3 を使う
    read_line_to_codes(In, AnimalCodes),
    %% 入力ストリームを閉じる
    close(In),
    %% バイトシーケンスを文字列に変換する
    utf8tring(AnimalCodes,Animal),
    %% 受け取った文字列の表示 
    write(Animal),nl,
    %% 知識のユニフィケーション           
    animal(Animal,Category),
    %% flush_output(),
    %% 文字列をバイトシーケンスに変換する
    utf8tring(CategoryCodes,Category),
    %% サーバーコンソールへの表示
    format('~s ~s  ~n', [Animal,Category]),
    %% フォーマットされた文字を出力ストリームに書き込む
    %% 与える文字列は、バイトシーケンスのリスト化されたものでなければならない
    %% ~sはCでいう %s 、 ~n は改行
    format(Out, '~s is ~s  ~n', [AnimalCodes,CategoryCodes]), 
    %% 出力ストリームを閉じる
    close(Out),
    %% プールからストリームを削除する
    delete_stream_from_pool(In).

冒頭の知識は、超簡単なものだが、日本語wikipediaの場合は、別ファイルになる。

このサーバーで最も困ったのは、ストリームの送受信は、すべてutf8のバイトシーケンスになっていることだった。ascii文字は、そのままでも良いが、マルチバイト文字に対応していない。前の記事で書いた日本語とバイトシーケンスを相互に変換するprolog手続きを作成しなければならなかった。Qittaには、片方向のものをあげたが、双方向に変換できるprologプログラムは、以下のようになる。

%%
%% utf8のbyte sequenceをprolog内の文字列に変換する
%% 逆に、文字列をutf8のバイトシーケンスに変換する
%% 

%% (例)
%% byte sequence
%% ライオン => [227,131,169,227,130,164,227,130,170,227,131,179]
%% lion => [108, 105, 111, 110]
%% アスキーとマルチバイトの混合
%% ラlイiオoンn = [227,131,169,108,227,130,164,105,227,130,170,111,227,131,179,110]
%% codepoint
%% ライオン => [12521, 12452, 12458, 12531]

%% TOPレベルpredicate, 双方向
%% utf8のバイトシーケンスを文字列に変換する
%% asciiが終わってマルチバイトも調べないために、最後にカットを入れている
%% 何れにしても、ここでバックトラックは不要なのでつけておく
%% 途中経過のコードポイントリストも出力する
utf8tring(L,X) :- var(X),getcodepoint(L,Y),
                         write(Y),nl,atom_codes(X,Y),!.
%% 文字列から、utf8のバイトシーケンスを取得する
utf8tring(S,X) :- var(S),atom_codes(X,L),
                         getutf8sequence(L,S),write(S),nl,!.

%% 再帰の終了条件
getutf8sequence([],[]).
%% コードポイントリスト(L), utf8バイトシーケンス(S)
%% アスキー文字の場合
getutf8sequence(L,[A|X]) :- [A|T] = L, A < 256, 
                         getutf8sequence(T,X). 
%% マルチバイトの場合 
getutf8sequence(L,Z) :- [M|T] = L, isoutf8(M,X), 
                         getutf8sequence(T,Y), append(X,Y,Z). 
%% マルチバイト文字のコードポイントをutf8の3バイトに変換する 
isoutf8(X,[C1,C2,C3]) :- C1 is X >> 12 /\ 15 \/ 224,
                         C2 is X >> 6 /\ 63 \/ 128,
                         C3 is X /\ 63 \/ 128.

%% 再帰の終了条件
getcodepoint([],[]).
%% アスキー文字の場合
getcodepoint(L,[H|X]) :- [H|T] = L
                ,isascii(H),getcodepoint(T,X).
%% utf8マルチバイトの場合
getcodepoint(L,[X1|X2]) :- headthree(L,Y,T),
                utf8iso(Y,X1),getcodepoint(T,X2).

%% マルチバイトの場合、頭の3個を取り出して、残りをTに入れる
headthree(L,[X1,X2,X3],T) :- [X1|T1]=L,
                [X2|T2]=T1,[X3|T]=T2.

%% 最高bitが0ならば、アスキー文字
isascii(A) :- (A >> 7) =:= 0.

%% 3バイトのutf8コードをコードポイントに変換する
%% 00001111 -> 15
%% 00111111 -> 63 
%% nth1 はリスト(L)の指定位置(N)の要素を取り出す(X) 
utf8iso(L,X) :- nth1(1, L, Y1), Z1 is 15 /\ Y1,
                nth1(2, L, Y2), Z2 is 63 /\ Y2,
                nth1(3, L, Y3), Z3 is 63 /\ Y3,
                X is Z1 << 12 \/ Z2 << 6 \/ Z3. 

こちらのプログラムもサーバープログラムと同時に読み込まれなければならない。その後に、ポートを決めてサーバーを立ち上げる。

テスト用に、クライアントプログラムをjavaで作成した。以下に参考例と示しておく。TCPに対応するクライアントならば、どのようなものでも良い。telnetでもいけると思う。

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;

/**
 *
 * @author washida
 */
public class JPClient {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        try {
            String server = "localhost";
            int port = 20000;
            Socket soc = new Socket(server, port);

            OutputStream os = soc.getOutputStream();
            PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(os,"UTF-8")));

            //pw.print("ライオン\r\n");
            //pw.flush();
            pw.print("キリン\r\n");
            pw.flush();

            InputStream is = soc.getInputStream();
            BufferedReader in = new BufferedReader(new InputStreamReader(is));
            String line;
            while((line = in.readLine()) != null){
                System.out.println(line);
            }
            pw.close();
            os.close();
            in.close();
            is.close();
        }catch (IOException e) {
            System.out.println("Exception: " + e);
        }
    }
    
}

実行例は次のようになる。

%% サーバー(prolog)側の端末

?- ['server.swi','utf8string.swi'].
true.

?- server(20000).
[12461,12522,12531]
キリン
[232,141,137,233,163,159,229,139,149,231,137,169]
キリン 草食動物  
[12521,12452,12458,12531]
ライオン
[232,130,137,233,163,159,229,139,149,231,137,169]
ライオン 肉食動物  

// クライアント(java)側の端末
キリン is 草食動物
ライオン is 肉食動物  

サーバー側には、変換過程を示すために、バイトシーケンスとコードポイントのリストが表示されている。サーバーは、port番号20000番で開いている。優先的に利用されていないポート番号ならんば、何番でも良い。また、ネットワークにつながっているクライアントであれば、どこからでもprologが処理する知識を獲得できるわけである。