Prolog: Prolog からの Ruby 呼び出し

# はじめに
Prolog 側から Ruby のメソッドを呼び出す、というのをやってみました。

動作環境

- Ubuntu Linux 14.04
- Ruby 2.1.5 p273
- SWI-Prolog version 6.6.4 for amd64
- スクリプト類
  - ruby-client.pro  (Prolog プログラム) ※クライアント (本稿末尾に掲載)
  - server.pro (Prolog プログラム)       ※Prolog サーバ
  - prolog_server.rb (Rubyスクリプト)    ※サーバ (本稿末尾に掲載)
  - prolog_parser.rb (Rubyスクリプト)    ※ミニ Prolog パーサ
  - prolog_proxy.rb (Rubyスクリプト)     ※Prolog サーバと通信用

ruby_client.pro、prolog_server.rb 以外は、以下の記事の流用です。
詳細はそちらをご覧ください。

- [Ruby: Prologで翻訳「時の蝿はひとつの矢を好む」][Qiita#1] 


# デモ (その1)

![prolog_ruby-2.png](https://qiita-image-store.s3.amazonaws.com/0/60624/7de55cb8-da5f-29d5-efb6-a2570d6d89a3.png)

上の図のように Ruby サーバに Prolog から TCP でメッセージを送信します。
Ruby サーバにはデモ用のレシーバオブジェクト(dummy_receiver)が仕込まれています。
dummy_receiver は引数を puts するだけのメソッド foo,bar,baz を持っています。

では、Ruby サーバを開始させます。

```shell-session
$ ruby prolog_server.rb    # デフォルトでポート 53340/tcp を LISTEN
                           # 起動したままになる。停止するには Ctrl-C
                           # --log ログファイル名 を付けて起動するとログ出力します
```

クライアントプログラムを読み込んで SWI-Prolog を起動します。

```shell-session
$ swipl -l ruby_client.pro
      :
  (Prolog が起動する)
```

Ruby サーバのメソッドを呼び出すには、以下の述語を使います。(使わなくてもできますが)

```prolog
  ruby(Pred)               % ruby/1 : Host = 'localhost', Port = 53340
  ruby(Pred, Port)         % ruby/2 : Host = 'localhost'
  ruby(Pred, Port, Host)   % ruby/3
```

SWI-Prolog のプロンプトから、やってみます。

```prolog
?- ruby(foo).                 % アトムの場合、引数なしでメソッドを呼び出す
true.

?- ruby(bar(a,b,c)).          % 述語の場合、述語の引数を渡してメソッドを呼び出す
true.

?- ruby(baz(a,f(x,y),[1,2,3],"ABC")).
true.

?- ruby(1).                   % 1 はアトム/述語以外なので無視される
true.
```

上を実行により、Ruby サーバ側で以下のように表示されます。

```
$ ruby prolog_server.rb
call method: foo(), args = 0
call method: bar(a,b,c), args = 3
call method: baz(a,{:f=>[:x, :y]},1,2,3,65,66,67), args = 4   # puts なのでこう見えますが、引数は 4 個です。
```

メッセージに対応するメソッドが呼び出されます。
引数は、ミニ Prolog パーサの解釈(Ruby表現)で渡されます。
`ruby(1)` のように、アトム/述語以外のものは無視されます。

---

# デモ (その2: Prolog もサーバにする)

- [Ruby: Prologで翻訳「時の蝿はひとつの矢を好む」][Qiita#1] 

上の記事でしたように Prolog もサーバにしてみることもできます。

![prolog_ruby.png](https://qiita-image-store.s3.amazonaws.com/0/60624/c00a3fa5-46ed-294b-b63c-79c876fea24a.png)

Ruby サーバの起動はデモ(その1)と同じです。

```shell-session
$ ruby prolog_server.rb    # デフォルトでポート 53340/tcp を LISTEN
                           # 起動したままになる。停止するには Ctrl-C
```

続いて Prolog サーバを起動します。

```shell-session
$ swipl -l server.pro -g 'create_sserver(53330).'  # 起動したままになる
```

Prolog サーバへの通信は Prolog::Proxy で行います。
pry を使います。ruby-client.pro があるディレクトリで pry を起動します。

```shell-session
$ pry -r prolog_proxy        # prolog_proxy.rb を require します。
    :
```

load メソッドで Prolog サーバに ruby-client.pro の内容を流し込みます。
その後で Prolog サーバに ruby/1 を問い合わせます。

```rb
[1] pry(main)> cd Prolog::Proxy
[2] pry(Prolog::Proxy):1> load ['ruby-client.pro']
=> ["./ruby-client.pro"]
[3] pry(Prolog::Proxy):1> q 'ruby(foo(a,b))'
=> "[ruby(foo(a,b))]"
```

Ruby サーバで dummy_receiver の foo が呼び出され、以下のように表示されます。

```shell-session
(Ruby サーバが起動しているターミナル)
call method: foo(a,b), args = 2
```

# デモ (その3: Proxy を drb デーモンにする)

- [Ruby: Prologで翻訳「時の蝿はひとつの矢を好む」][Qiita#1] 

上の記事でしたように Proxy を drb デーモンしてみます。
リモートの Ruby から drb で Proxy にアクセスして Prolog サーバを操作できます。

![Prolog-Ruby-mini.png](https://qiita-image-store.s3.amazonaws.com/0/60624/3f8b7e35-bb8d-888b-793b-818c0407bbc2.png)

基本的に上の記事と同じことをしているので手順のみ掲載します。

```shell-session
: ターミナル#1 : Ruby サーバ起動
$ ruby prolog_server.rb
                                # 起動したままになる
```
```shell-session
: ターミナル#2 : Prolog サーバ起動
$ swipl -l server.pro -g 'create_server(53330).'
                                # 起動したままになる
```
```shell-session
: ターミナル#3 : Prolog サーバに ruby_client をロード
$ ruby -r prolog_proxy -e 'Prolog::Proxy.load ["ruby-client.pro"]'
```
```shell-session
: ターミナル#4 : Proxy デーモン(drbサービス)起動
$ ruby prolog_proxy.rb --drb --daemon  # バックグランドで起動
```
```shell-session
: ターミナル#5 (リモート) : pry で操作
$ pry -r drb             # drb を require
    :
[1] pry(main)> px = DRbObject.new_with_uri 'druby://:53331'
=> ...
[2] pry(main)> px.q 'ruby(foo(x))'
=> "[ruby(foo(x))]"
```

上の操作により、ターミナル#1 で Ruby サーバの処理結果が出力されます。

```shell-session
    :
call method: foo(x), args = 1
```

# サーバへのレシーバの設定の仕方
サーバには、任意のレシーバオブジェクトを設定できます。

```rb
require 'prolog_server'

class MyClass       # レシーバにするオブジェクトのクラス定義
  # 適当にメソッド定義する
  # def hoge(...) ... とすると Prolog側から `?- ruby(hoge(...)).` で呼び出せる
end
my_receiver = MyClass.new
````

上のように作ったレシーバ(my_receiver) を以下のいずれかの方法で指定します。

```rb
th = Prolog::Server.start(my_receiver)
th.join            # 戻り値はサーバの Thread オブジェクトなので join できる
```
```rb
Prolog::Server.daemon(my_receiver)  # この場合、デーモンになる
```
```rb
server = Prolog::Server.new         # インスタンスを作って指定する方法
th = server.start(my_receiver)
th.join
```
```rb
server = Prolog::Server.new
server.daemon(my_receiver)          # この場合、デーモンになる
```

レシーバのメソッド呼び出し時の例外をハンドルする場合は、以下のようにします。

```rb
server = Prolog::Server.new             
       # on_error でハンドラを指定する。引数はキャプチャする例外タイプ
server.on_error(NoMethodError) {|e| puts "Exception: #{e.class}" }
th = server.start(my_receiver)
th.join
```

# バイト配列と文字列の変換
SWI-Prolog は引数の文字列を送信する時に、整数(バイト値)のリストとして送るようです。

```rb
[63] pry(Prolog::Proxy):1> q 'ruby(foo("ABC"))'
=> "[ruby(foo([65,66,67]))]"
```

Ruby サーバのミニ Prolog パーサは、文字列は String に、リストは Array に変換するので、レシーバの引数には整数(バイト値)の Array として渡されます。

バイト配列と文字列の変換が必要な場合は、レシーバで行います。

```rb
# 変換の例
p [65,66,67].pack "C*"       #=> "ABC"
p "ABC".unpack "C*"          #=> [65, 66, 67]
```
```rb
# バイトの配列か判定する例
arg = [65,66,67]

p arg.kind_of?(Array) && arg.all? {|n| (0..255).include? n }    #=> true
```

ちなみに、拙稿「[Ruby: method_missing を使ってみた][Qiita#2]」の KindOfTest を使うと判定は以下のようにできます。

```rb
require 'kind_of_test'
class Object ; include KindOfTest ; end

p [65,66,67].array? &:byte?          #=> true
p [65,66,67,256].array? &:byte?      #=> false
```

# スクリプト

クライアント (Prolog プログラム)

```prolog:ruby_client.pro
ruby(Req) :- ruby(Req, 53340).
ruby(Req, Port) :- ruby(Req, 'localhost', Port).
ruby(Req, Host, Port) :- create_client(Req, Host, Port).

create_client(Req, Host, Port) :-
    setup_call_catcher_cleanup(tcp_socket(Socket),
                               tcp_connect(Socket, Host:Port),
                               exception(_),
                               tcp_close_socket(Socket)),
    setup_call_cleanup(tcp_open_socket(Socket, In, Out),
                       request_to_server(Req, In, Out),
                       close_client_connection(In, Out)).

close_client_connection(In, Out) :-
    close(In, [force(true)]),
    close(Out, [force(true)]).

request_to_server(Req, _, Out) :-
    write(Out, Req).
```

サーバ (Ruby スクリプト)

```rb:prolog_server.rb
require 'socket'
require 'resolv-replace'
require 'logger'
require 'prolog_parser'

module Prolog
  class Server
    HOST, PORT = '0.0.0.0', 53340

    def self.daemon(*args, &block)
      new(*args, &block).daemon
    end

    def self.start(*args, &block)
      new(*args, &block).start
    end

    class ServerLogger < Logger
      def level=(x)
        x = Logger.const_get(x) if %i(DEBUG INFO WARN ERROR FATAL).include?(x)
        super(x)
      end
    end

    attr_accessor :log
    attr_writer :logger
    def logger; @logger ||= (log ? ServerLogger.new(log) : null) ; end

    attr_accessor :host, :port, :receiver

    def initialize(receiver=nil, port:nil,host:nil, logger:nil,log:nil, &block)
      tap {|my| my.receiver, my.port, my.host, my.logger, my.log =
                   receiver,    port,    host,    logger,    log }

      instance_eval(&block) if block
    end

    attr_reader :thread
    
    def daemon(*args, &block)
      stop

      logger.info "start daemon"
      Process.daemon
      service_loop *args, &block
    end

    def start(*args, &block)
      stop

      logger.info "start server: pid = #{$$}"
      @thread = Thread.new { service_loop *args, &block }
    end

    def stop
      return unless @thread

      logger.info "stop server: pid = #{$$}"
      @thread.kill rescue nil
      @thread = nil
    end

    private
    def service_loop(receiver=nil, port:nil, host:nil)
      port     ||= (self.port     || PORT)
      host     ||= (self.host     || HOST)
      receiver ||= (self.receiver || null)

      logger.info "accept loop: host = #{host}, port = #{port}"

      Socket.tcp_server_loop(host, port) do |conn, *|
        Thread.new do
          begin
            serivce(receiver, conn)
          ensure
            conn.close
          end
        end
      end
    end

    def serivce(receiver, conn)
      msg = conn.read           ; logger.debug "request(raw) : #{msg}"
      req = Prolog.to_ruby(msg) ; logger.debug "request(ruby): #{req}"

      pred?(req) ? receiver.send(req.keys.first, *req.values.first) :
      atom?(req) ? receiver.send(req)                               : nil
    rescue Exception => e
      logger.error "exception: #{e}"
      error_notify(e)
    end

    def pred?(x)
      x.kind_of?(Hash)               &&
      x.size == 1                    &&
      x.keys.first.kind_of?(Symbol)  &&
      x.values.first.kind_of?(Array)
    end

    def atom?(x)
      x.kind_of?(Symbol)
    end

    def null
      @null ||= Class.new{ def method_missing(*) ; end }.new
    end

    public
    def on_error(exception_type=Exception, &block)
      handlers[exception_type] << block if block
    end

    private
    def handlers
      @handlers ||= Hash.new {|h, k| h[k] = [] }
    end

    def error_notify(exception)
      handlers.select{|type, *| exception.kind_of? type }.values.reduce(:+)
              .each  {|handler| handler.(exception) }
    end
  end
end

if __FILE__ == $0
  require 'optparse'
  require 'ostruct'

  class DummyReceiver
    def foo(*args)
      puts "call method: foo(#{args * ','}), args = #{args.count}"
    end
    def bar(*args)
      puts "call method: bar(#{args * ','}), args = #{args.count}"
    end
    def baz(*args)
      puts "call method: baz(#{args * ','}), args = #{args.count}"
    end
  end

  dummy_receiver = DummyReceiver.new

  opt = OpenStruct.new ARGV.getopts '', 'port:','host:','log:','daemon'

  if opt.daemon
    # (注意) デーモンにすると標準出力などが切り離されます
    Prolog::Server.daemon(dummy_receiver,
                          port:opt.port, host:opt.host, log:opt.log)
  else
    server = Prolog::Server.new(dummy_receiver,
                                port:opt.port, host:opt.host, log:opt.log)
    server.on_error {|e| puts e }
    server.logger.level = :DEBUG
    server.start.join
  end
end
```

[Qiita#1]: http://qiita.com/lnznt/items/72abba6b045ef4ffafae
[Qiita#2]: http://qiita.com/lnznt/items/ee2f7763ee882826da63