Oliverの日記: パーサージェネレータ 2
ちょこっとづづ進んでる大学の研究実習で書いてるRubyなオブジェクトリポジトリ(コードネームVapor)の基本機能がここ1ヵ月の集中でひととおり完成した。XMLなクラス説明からSQL DDLを作成した後、クラスにVapor::Persistableモジュールをincludeするだけで、そのクラスのオブジェクトをDBから出し入れできる。今週末は検索機能を実装した。当然ながら、SQLのWHERE文をそのまま書かせるわけにはいけないので、それにとっても近い独自のとってもシンプルなキュエリー言語を作った。
objects = pmersistence_mgr.query( Foo, "bar = ? AND baz = ?", [ 42, "very"] )
といった感じだ。これからキュエリーを表現するオブジェクトを作成してSQLのWHERE文に変換するわけだ。自力でパーズするのは、前にPostgreSQL Array -> Ruby Array変換ルーチンを書いたときに懲りているし、もちょっと複雑なオートマトンを自分で管理するのは面倒なので、今回はパーザージェネレータを使った。Rubyの定番パーザージェネレータといえばraccだが、RAAを漁ったところ、rockitというのを発見。raccがyacc風のイベント発生型パーザジェネレータならば、rockitはAbstract Syntax Tree(AST)生成機で、文法とアクションが分離できる。今回の文法をそれぞれの形式で書いてみたたものの中核だけ抜粋すると、raccだと
class Vapor::QueryParser
prechigh
left BOOL
preclow
rule
statement : '(' statement ')'{ result = val[1] } | IDENT COMP PLACEHOLDER { result = BasicQueryStatement.new( val[ 0], val[1], @arguments[@ph_count] ); @ph_count += 1 }
| statement BOOL statement { result = ComplexQueryStatement.new( val[0], val[1], val[2] ) }
end
[...] def parse( query )
@tokens = []
query.scan(/\w+|=|\?|\(|\)/).each{ |token|
case token
when /\A\w+/
puch @tokens [:IDENT, $&]
[...]
end
}
do_parse
end
といったところだ。resultを使ったracc固有の約束ごとが必要なのと、アクションが文法に紛れているのが特徴だ。半面、rockitだと
Grammar VaporQuery
Tokens
Blank = /(( )|(\t)|(\v))+/ [:Skip]
Identifier = /\w+/
Comp_Op = /=/
Bool_Op = /AND/
Placeholder = /\?/
Productions
Stmnt -> '(' Stmnt ')' [Stmnt: _,stmnt,_]
| Stmnt Bool_Op Stmnt [ComplexStmnt: stmnt1, op, stmnt2]
| Identifier Comp_Op Placeholder [BasicStmnt: identifier, op, placeholder]
Priorities
left(ComplexStmnt)
----
def query_eval(ast)
case ast.name
when "Stmnt"
query_eval( ast.stmnt )
when "ComplexStmnt"
return ComplexQueryStatement.new( query_eval( ast.stmnt1 ), ast.op[0], query_eval( ast.stmnt2 ) )
[...]
end
end
q = query_eval( QueryParser.parser.parse( query ).compact! )
といったところだ。正直いって、rockitのほうが素敵だと思うが、ランタイムに依存しない形式が作成でき、(今日の時点で)apt-get一発でインストールできるという理由でraccにした。rockitはこれからも要注目だな。
さてと、次はトランザクション処理の実装か。その後に残る課題はバージョン履歴システムとワークフロー支援の長期トランザクション (バージョン権利におけるブランチという構想)だ。暇があれば、なんか認証システムとオブジェクトの読み書きに関する権限システムも考えたい。さらに頑張るとして期末までの2ヵ月で完成するんかい。
ちなみに、最近、Ruby Array -> PostgreSQL Arrayの変換ルーチンも書いた。こっちはパーズしなくてよかったので楽だったが、バックスラッシュに泣いた。エスケープなんて仕組みが必要ない世の中が欲しい。前のパッチと共に再びruby-dbiリストに投げたところ、今度はマージされる可能性が高そうだ。Perl DBIやJDBCのPostgreSQLドライバにすらない高度な機能だもんね、売り文句になる。しかし、PostgreSQLのArrayって誰も使ってないんかな、リテラルの表現以外は便利なのに。
Query言語? (スコア:1)
SyntaxTreeをRubyのObjectとして作って与えるという形態とか。
Rubyくらい記述力のある言語なら、専用の文を書くようにせず、
Query Object(?)をその場で組み上げるという形態にしても、
記述し易さはあまり劣らないんじゃないか?と想像します。想像だけですが。
#C言語ベースでそれをやらせやがるライブラリに日夜苦しめられてるんでG7
ふと思ったけど、括弧をIteratorで表現する、とか。
class Where
def initialize
@data=[]
end
def eq(name, value)
@data name.to_s
@data '='
@data value.to_s
self
end
def and
@data 'AND'
self
end
def kakko # block
@data '('
yield self
@data ')'
self
end
def to_s
@data.join(' ')
end
end
puts Where.new.eq("hoge", 10).and.eq("fuga", "20")
puts Where.new.kakko{|w| w.eq("hoge", 10)}.and.eq("fuga", "20")
w2=Where.new
puts w2.kakko{w2.eq("foo", "bar").and.kakko{w2.eq("zot", "aaa")}}
うーん。やっぱりちっとも書きやすくないか(笑)
Re:Query言語? (スコア:1)
トランザクションとバージョン管理はまだだけど、現状でも有用と思えるところまできたので、ドキュメントをちょっと掃除したら、一度パブリックリリースしようかな、と思ってます。READMEを書くのが一番大変だ。