5
5

More than 5 years have passed since last update.

Prolog: Prolog からの Ruby 呼び出し

Posted at

はじめに

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 以外は、以下の記事の流用です。
詳細はそちらをご覧ください。

デモ (その1)

prolog_ruby-2.png

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

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

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

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

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

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

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

SWI-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 もサーバにする)

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

prolog_ruby.png

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

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

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

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

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

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

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

[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 が呼び出され、以下のように表示されます。

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

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

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

Prolog-Ruby-mini.png

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

: ターミナル#1 : Ruby サーバ起動
$ ruby prolog_server.rb
                                # 起動したままになる
: ターミナル#2 : Prolog サーバ起動
$ swipl -l server.pro -g 'create_server(53330).'
                                # 起動したままになる
: ターミナル#3 : Prolog サーバに ruby_client をロード
$ ruby -r prolog_proxy -e 'Prolog::Proxy.load ["ruby-client.pro"]'
: ターミナル#4 : Proxy デーモン(drbサービス)起動
$ ruby prolog_proxy.rb --drb --daemon  # バックグランドで起動
: ターミナル#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 サーバの処理結果が出力されます。

    :
call method: foo(x), args = 1

サーバへのレシーバの設定の仕方

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

require 'prolog_server'

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

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

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

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

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

バイト配列と文字列の変換

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

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

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

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

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

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

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

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 プログラム)

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 スクリプト)

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
5
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5