Edited at

Ruby: ミニ Prolog パーサ (改)

More than 3 years have passed since last update.

以下の記事で作ったミニPrologパーサを改修しました。

以前の課題はおおむね解消しましたが、まだまだ実験的な実装です。

できたら、そのうちにまた改修します。


スクリプト

GitHub にあります。

ソース本体は prolog_parser.ry です(随時アップデートしています)。

これを racc でコンパイルして prolog_parser.rb を作って使います。


Ruby 上での Prolog の表現

Prolog
Ruby
Prolog側の例
Ruby側の例

述語
Hash (形式: {Symbol=>Array})
pred(a,b,10)
{pred: [:a,:b,10]}

リスト
Array
[a,b,10]、[]
[:a,:b,10]、[]

リスト(「H|T」形式)
Hash (形式: {Array=>Array})
[a,b|T]
{[:a,:b]=>[{nil=>:T}]}

リスト(文字列)
String
"abc"
"abc"

数値(整数)
Integer
1、-10
1、-10

数値(小数)
Float
1.1、-10.5
1.1、-10.5

アトム
Symbol
a、'ABC'
:a、:"ABC"

変数
Hash (形式: {nil=>Symbol})
X、_abc、_
{nil=>:X}、{nil=>:_abc}、{nil=>:_}

「{}」で囲まれた項
Hash (形式: {true=>項の表現})
{a(b,c)}
{true=>{:a=>[:b,:c]}}


  • 変数の表現は Symbol から Hash に変更しました。それに伴い、「'」で囲まれたアトムの表現も変更しました。

  • 「リスト(「H|T」形式)」、「「{}」で囲まれた項」は今回追加しました。


実行例

ビルド

$ make

racc -g -o prolog_parser.rb prolog_parser.ry

コマンドライン実行

:

: 実行例 (コマンドライン引数に渡す文字列には最後の「.」は含めません)
:

$ ruby prolog_parser.rb "abc"
:abc

$ ruby prolog_parser.rb "a(abc)"
{:a=>[:abc]}

$ ruby prolog_parser.rb "[1,2,3]"
[1, 2, 3]

$ ruby prolog_parser.rb "a(X) :- b(X),!"
{:":-"=>[{:a=>[{nil=>:X}]}, {:","=>[{:b=>[{nil=>:X}]}, :!]}]}

$ ruby prolog_parser.rb ",(,,,),,(,,,,,)"
{:","=>[{:","=>[:",", :","]}, {:","=>[:",", :",", :","]}]}

$ ruby prolog_parser.rb -v "a(abc)"
[:FUNCTOR, "a"]
[:ATOM, "abc"]
[")", ")"]
[".", "."]
nil
{:a=>[:abc]}

コマンドラインオプション

  #

# $ ruby prolog_parser.rb [options]
#
# options are:
# -v トークンキューをダンプ出力する
# -d パーサデバッグコードを出力する
#


Prolog::Parser#parse メソッドの使い方注意

  #

# パーサへのソースの指定の仕方 (同時に指定した場合、下の方が優先)
#
# parse メソッドに指定する方法
#
# parser.parse(term: str) .... 項(文字列、最後のピリオド(.)不要)
# parser.parse(line: str) .... 行(文字列、最後のピリオド(.)必要)
# parser.parse(file: path) ... ファイル名(文字列)
# parser.parse(io: io) ....... read により入力文字列を読み出せる IO
# parser.parse(enum: enum) ... next により入力文字列を読み出せる Enumerator
# parser.parse(reader: pr) ... call により入力文字列を取得できる Proc
# parser.parse(&pr) .......... (上と同じ)
#
# アクセサで指定する方法
#
# parser.term = str
# parser.line = str
# parser.file = path
# parser.io = io
# parser.enum = enum
# parser.reader = reader
#
# parser.parse
#
# ----
#
# parse の戻り値は、解析結果を出力する Enumerator である
#
# (I) ワンショット(解析結果を1回のみ取り出す)
#
# (例)
# pp result = parser.parse(line: str).first
#
# (II) イテレート(解析結果を繰り返し取り出す)
#
# (例)
# parser.parse(io: f).each {|result| pp result }
#
# pp results = parser.parse(io: f).entries
#


Prolog サーバに問い合わせ

上記の記事でもやりましたが、Prolog パーサに問い合わせしてみます。

通信のためのモジュール(以下)を作りました。


prolog_proxy.rb

require 'socket'

module Prolog
class Proxy
attr_accessor :host, :port

def initialize(port:, host:'localhost', &block)
tap {|my| my.host, my.port = host, port }

instance_eval(&block) if block
end

# Prolog サーバのデータベースに節を問い合わせをする
def inquire(query, &conv)
conv ||= -> q { q }
TCPSocket.open(host, port) do |s|
###s.puts "#{conv.(query)}."
###s.gets
# SWI-Prolog 決め打ちでサーバが EUC-JP として処理する
s.puts "#{conv.(query.encode('EUC-JP'))}."
s.gets.encode('UTF-8', 'EUC-JP')
end
end

# Prolog サーバのデータベースに節を追加する
def insert(clause)
query =
clause =~ /-->/ ? "dcg_translate_rule((#{clause}),X),assert(X)" :
"assert((#{clause}))"
inquire(query)
end
end
end

if __FILE__ == $0
require 'pp'
require 'prolog_parser'

prolog = Prolog::Proxy.new port:3333

pp Prolog.to_prolog Prolog.to_ruby term:prolog.insert("person(socrates)")
pp Prolog.to_prolog Prolog.to_ruby term:prolog.insert("mortal(X) :- person(X)")

pp Prolog.to_prolog Prolog.to_ruby term:prolog.inquire("mortal(Who)")
end


実行例

サーバの実行。(Prolog プログラムserver.proは上の記事で使ったもの。処理系は SWI-Prolog)

$ swipl -l server.pro -g 'create_server(3333).'

# サーバは起動したままになる...

問い合わせ。

$ ruby ../prolog_proxy.rb 

"[assert(person(socrates))]"
"[assert(:-(mortal(_G61),person(_G61)))]"
"[mortal(socrates)]"

サーバ側で、「mortal(...) :- person(...)」のように中置形式で応答していますが、 Ruby 側では構文解析をして、一旦オブジェクトにしてから表示しているので元の形式を忘れます。

なので、「:-(mortal(...),person(...))」の形式で出力されています。


おわりに

本稿内容の動作確認は以下の環境で行っています。


  • Ruby 2.1.5 p273

  • SWI-Prolog version 6.6.4 for amd64

  • Ubuntu Linux 14.04