Posted at

Prolog: Prolog からの Ruby 呼び出し

More than 3 years have passed since last update.


はじめに

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)

上の図のように 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 もサーバにしてみることもできます。

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 サーバを操作できます。

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

: ターミナル#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