後藤謙太郎 <URL:http://www.notwork.org/~gotoken/mag/cmagazine/>
註: この文書は『C MAGAZINE』2000年10月号 に掲載された記事の元となるものに手を加えたものです.
記事中のプログラムを一つずつファイルにしたものもあります(→list)
中田さんのおかげでNetscapeでも読めるようになりましたが,まだちょっとカッコ悪いです。「CSSの書き方がなっとらん、こう書け」というお便りは歓迎します。
間違いを見つけたら,gotoken@notwork.org宛に御連絡くださると喜びます。
Copyright(c) 2000 by GOTO Kentaro. All rights reserved.
こんにちわ, RD ってなに? ( RD の規則, RDの強み ), とりあえず RD を抜き出す, 作戦を立てよう ( 大まかな流れ ), ブロック解析, ( インターフェイス ), 実装 ( メインループ, 各ブロックの処理, 雑関数, 構文木の詳細 ), まとめ, 謝辞, 参考資料
ごとけんです。もうすぐ11月。秋といえば紅玉りんごの旬ですね。紅玉は宝石 rubyの訳語でもあるわけですが、先月、難産の末にようやく Ruby 1.6.0 がリリースされました。すでにお使いの方も多いと思います。てなわけで、今回からこの連載のターゲットは 1.6 系の最新版とします。まだ1.4系をお使いの方はこの機会に是非入れ換えることをお勧めします。1.6の詳細は近く出版されるリファレンスマニュアルなどでも知ることができますけど、せっかくなのでここで新機能を1つだけ紹介しましょう。
これまでRubyには「あるクラスのインスタンス間で共有される変数」、いわゆるクラス変数がありませんでした。そこで定数コンテナ*1で代用していたわけですが、ついにクラス変数が導入されました。Ruby の変数の接頭辞はスコープを表すものになっていますが、クラス変数の接頭辞には「@@」が割り当てられました。これはインスタンス変数との類似性や、使用頻度を考えての結果です。ありがちだけどシンプルな例をList 1に挙げておきます。
-- List1 objnum.rb: インスタンスの個数をクラス変数で
class Foo
@@cnt = 0
def initialize
@@cnt += 1
end
def n
@@cnt
end
end
if __FILE__ == $0
p Foo.new.n #=> 1
p Foo.new.n #=> 2
end
それと重要な変更点の一つに、デフォルトの文字コードが none、つまり「無し」になったことが挙げられます。自分のRubyのデフォルトの文字コードを次のようにして調べておきましょう。文字コードの詳細はこの連載の第1回(Cマガ8月号)を御覧ください。
% ruby -e 'p $KCODE' "NONE"
ところで 1.6 リリースを記念するわけではありませんが、11月の29,30日に Perl/Ruby Conference 2000 という集会が京都で開催されます[PRC]。ぼくも参加する予定でして、たくさんのRuby好きのみなさんにお会いできるのをいまから楽しみにしています。
さてさて、この連載ではこれまで、どちらかといえば断片的な事柄ばかりを紹介してきたんですが、風にのって聞こえてきた読者の声に従って今回はモノを作ることをゴールにしたいと思います。確かに実際のプログラムを見ないと分かりにくいってとこはありますよね。そして、利用頻度を考えてテキスト処理を題材にとった次第です。あ、何を作るかというと、RDの変換器です。
RD とは Ruby Document の略で、Rubyのプログラムに埋め込まれたコメントの書き方です。HTMLやRoffなどの他のマークアップ言語に変換することを狙っていて、このおかげで、プログラムのなかにマニュアルのソースを読みやすい形で含めることができます。Perlユーザには「PODのようなもの」といえば分かっていただけるかも知れません。
RD も Ruby の作者のまつもとさんがはじめた書き方で、POD とPlain2 を参考にして可読性と書きやすさを重視したものになっています。大雑把にいうと、見出しを付けること、箇条書き、プログラムの貼り付けだけができます。また Plain2に由来する書式を使って強調なども用意されています。機能が単純なので規則もシンプルなことから、普通の文書を書くのにもなかなか便利です。そんなわけで実はこの原稿も RD で書いています。
RD の規則は RDtool *2 の作者でもある Tosh さんが提案文書の形でまとめてくれています[JARS]。簡単に紹介しましょう。ここではブロックに話を絞って、RDで書いたRDの要約(List 2) を示します。たとえば「= RDとわ」と書いてあるのは、「=」ではじまるので見出しで、「=」が1個なので最も大きな見出し、HTMLでいえばH1要素に相当します。次の行はTextBlockといって普通の文(HTMLのP要素相当)です。また、「*」ではじまるのは見てのとおり箇条書です。
-- List2 rdsample.rb: RDで書いたRDの要約
=begin
= RDとわ
Rubyのドキュメントの書き方です。
従うといろいろ便利です。
== 規則
だいたい次のとおりです。
* 「=begin」と「=end」の間に書く。
* ブロックの並びからなる。
* ブロックには次の7つがある:
(1) Headline (見出し)
(2) Include (差し込み)
(3) TextBlock (段落)
(4) Verbatim (そのまま)
(5) ItemList (箇条書き)
(6) EnumList (番号付箇条書き)
(7) DescList (小見出付箇条書き)
* リストはその名を関した項目の列
(例:ItemListItemはListItemの列)。
* MethodListもあるが、現在
改良の余地が検討されている。
* List系ブロックは入れ子にできる。
範囲はインデントで表現。
* 空行が段落の区切り。
* 「=」で始まる行は見出し。
* 「*」で始まる行は箇条書。
* 「(1)」などで始まる行も箇条書。
* インデントされてるのは貼り付け。
* ブロックにはインラインが使える。
* インラインは ((...)) のような
形をしている。
== ブロックの表し方
先頭の文字で決まります。
: Headline
等号(=)ではじまる。
先頭に空白は許されない。
続けて見出し自体を書く。
=の数で見出しの深さを表す。
: Include
三つの小なり(<<<)にファイル名
: ItemListItem
星(*)ではじめる。
: DescListItem
コロン(:)ではじめる。その
後ろは見出し(Term)で、次の行
からが説明文(Description)に
なる。
: EnumListItem
「(1)」のように数字を括弧で
くくる。
: Verbatim
余分にインデントされた行。
: TextBlock
余分なインデントがなくて上に
該当しないものは地の文と
みなされる。
=end
List 2 をみて分かるようにフォーマットしなくてもとても読みやすいものです。詳しくみてみると、RDのかたまり(ブロック要素) は空行でない次の行の先頭部分を見る事で終りが来たかを判定できることも分かります。
ところで、このRDのような英文と和文の混在するテキストの入力にはSKK、ゆでたまご*3あるいはT-Codeのような方法が便利だと思います。
RDのメリットはいろいろあるのですが、たとえばHTMLとくらべるとタグに相当するものをを打つことがとても少ないうえに、テキストの見た目が構造を表現するので、文法的な間違いを犯すことが滅多にないことが上げられます。
RDはあらかじめ機能を限定しているので、RDだけでなんでもできるわけではないけれど、その分、入力も容易だし利用者が覚えることはとても少なくてすみます。他のフォーマットに対する一種のオーサリングツールと思ってもよいかも知れません。例えば配布用のXML文書をRDから生成するということも考えられます。
ただし、RDの文法要素には名前がついてはいるものの、具体的にどう変換するかはユーザに任せることになっていてます。しかしそれほど遠くない将来、 Rubyに標準のRD変換器が添付されると思われます。
さて、まずは「=begin」と「=end」に挟まれた部分を抜き出す所からはじめましょう。やり方はいろいろあると思いますが、ここでは1行ずつ読むことにし、いまRD を読んでいるのか、プログラムの部分を読んでいるのかを表す変数を使って切り分けてみます。
List 3 は、Rubyプログラムを読んで、RD の部分だけを出力するモノです。
where がいま読んでいるのがRDなのかどうかを表しています。先月号で紹介したようにRubyの case は === を使って判定するのでいろいろな場合わけに使えます。
-- List3 getrd.rb: 入力ファイルの=beginと=endのあいだを抜き出す
#!/usr/bin/env ruby
state = :Ruby
while line = gets
case where
when :Ruby
next if /^=begin/ !~ line
where = :RD
when :RD
case line
when /^=end/
where = :Ruby
else
puts line
end
end
end
ここで :RD と :Ruby というのが登場しますが、これは
Symbol オブジェクトのリテラルです。Ruby 1.4 ではこのリテラルはインタプリタ内部で対応する整数を表していましたが、1.6 では Symbol
という別のクラスになりました。List 3 では String を使ってもいいんだけど、文字列である必要もないので Symbol を使ってます。
List 2 をどうやって発展させればよいんでしょう。case line のところをどんどん拡大する? うーん、なんか大変そうです。やらないといけないことをすこし落ち着いて考えてみましょう。
まずブロックの解析はインデントを見ていけば良いでしょう。一行ごとに見ていくなら、いまどのブロックを見ていたかをスタックに積んでいくのが良さそうです。
一方インラインはどうすれば良いでしょうか。例えばList 4はインラインを含む文字列を分解するものです。ここでの分解とは [種類, 文字列] の形をした配列を要素とする配列を返すこととしています。ここでやっているようにクラスデザインをする場合にとりあえず配列で組むことが出来ることもありますね。粒度*4 によっては配列のままでもいいかも知れません。
untilの中身を少し説明しておきましょう。パターンにマッチした場合(if
pat =~ buf)は、まずマッチした箇所より前の部分 $' がカラでなければその部分の種類を :String として結果の配列 res 格納します。それからどのパターンにマッチしたかによって場合わけし、そのインライン要素の種類を表すシンボルと、マッチした文字列からなる配列を res
に突っ込み、残り($')を buf に再設定します。この buf がカラになるまで (until buf.empty?) は繰り返します。マッチしなかった場合(else)は、そのなかにはもうインラインはないわけですから、その時点の buf すべてを :String) としています。
もっともここまで厳格にやる必要はないかも知れません。もしも構造を返す必要がなく、さらにインラインが入れ子にならないという制限がついていれば、 String#gsub! で十分です。逆にこれでは足りないのかも知れません。たとえば相互参照*5を実現するには文書内部を参照する場合には2-passは必要でしょうし、外部を参照する場合はとくにマニュアルなどはURLよりも簡便な方法が欲しいものです。
-- List4 inlineperser0.rb: インライン解析の雛型
def inlineperser0(str)
o = [
'\{\s*(.+?)\s*\}',
'%\s*(.+?)\s*%',
'-\s*(.+?)\s*-',
'\*\s*(.+?)\s*\*'
].join('|')
pat = /\(\((?:#{o})\)\)/m
res = []
buf = str.dup
until buf.empty?
if pat =~ buf
unless $`.empty?
res.push [:String, $`]
end
if $1
res.push [:Code, $1]
elsif $2
res.push [:Kbd, $2]
elsif $3
res.push [:Note, $3]
elsif $4
res.push [:Em, $4]
end
buf = $'
else
res.push [:String, buf]
break
end
end
res
end
図5に実行例を示しておきます。
-- 図5 List 4の実行例(zshの場合)
% ruby -Ke -r inlineperser0.rb -e '
elements = inlineperser0 <<EOF
「(({cat}))」と書きます。また、コマンドラインに入力する cat は
「((%cat%))」と書けば区別されます。また強調は「((*強調*))」
のように書き、注を要れるときは「RD((-Ruby Documentの略-))」のよ
EOF
elements.each{|i| p i}
'
[:String, "「"]
[:Code, "cat"]
[:String, "」と書きます。また、コマンドラインに入力する cat は\n「"]
[:Kbd, "cat"]
[:String, "」と書けば区別されます。また強調は「"]
[:Em, "強調"]
[:String, "」\nのように書き、注を要れるときは「RD"]
[:Note, "Ruby Documentの略"]
[:String, "」のよ\n"]
%
以上の考察から今回はRubyだけで出来ることにします。戦略としては、RDtool にならってRDからまず中間表現に変換し、その中間表現から各種のフォーマットに変換するという手法をとりましょう。こうすると複数のフォーマットをターゲットにするとき、再利用が楽になります。将来インラインの解析をRaccに乗り変えて拡張できるような作りにしておきたい気もしますので、中間表現を得る際には入力テキストをブロック単位に分解してから、インラインを処理することにします(図6)。
-- 図5 処理の流れ
+----+
| RD |
+----+
↓
( ブロック解析 )
↓
( インライン解析 )
↓
+------------------------+
| 中間表現としての構文木 |
+------------------------+
↓ ↓
+------+ +-------+ ‥‥
| HTML | | LaTeX |
+------+ +-------+
それから名前も考えておきましょう。rd2という名前は紛らわしいので、rd2を日本語に訳して rdo とします。
List 2 で見たような文字列にブロックの開始は、行頭の文字と、インデントで分かります。一方、ブロックの終りは現在のブロックが何であるかによります。例えば、次の例は2つの段落です。
----------------------------------- これは文です。 これも文です。 -----------------------------------
次の例は1つの段落と1つのVerbatimです。
-----------------------------------
短いスクリプトを見ていましょう。
#! /usr/bin/env ruby
puts "おしゃべり!おしゃべり!"
-----------------------------------
このようにVerbatimの場合は空行で終りにしたくありません。Verbatimはインラインも使わないですし特別扱いしたほうが良さそうです。
最後にリストをみてみましょう。
-----------------------------------
* これはItemListItemです。
ここはItemListItemの続きで
次の段落でしょう。
ここはItemListItemの中の
Verbatim ですね。
* これは次のItemListItemです。
-----------------------------------
リストの場合は、リストを表す目印「*」、「(1)」、「:」の次に来る文字の位置を基準にインデントの勘定が補正されます。この「目印の次に来る文字の位置」はBaselineと呼ばれています。終る条件はBaselineよりも浅いインデントが行なわれたときとなります。また一般化するために各リストの項目は、目印にブロック要素がひっついたものとみなし、TextBlock にも Baseline があると考えます。
また、やや面倒な問題としては、空白文字 (/\s/) に関連することです。まず、/^$/ と /^\s+$/ にそれぞれマッチする行たちを区別するか否かという問題があります。これらの行はテキストエディタの上では区別がつかないことが多いですから、とりあえず区別しないことにしましょう。それとタブ文字と、スペース文字の区別も厄介です。とりあえず行頭の空白文字に含まれるタブ文字は適宜、指定された長さのスペース文字に置き換えることにします。
複数のフォーマッタに出力できるようにする場合、出来るだけ簡単にフォーマッタを書きたいですね。ただ、相互参照を行なうようにする場合は実際はフォーマッタといってもラベルをブロック解析の際に仕込んだりする必要があります。このような機能もここでは割愛します。
そうすると、フォーマッタはList 6程度で書けるようになるわけです。この例では、一番最後の行が実行文でARGFをBlockParserに読ませ、parseし、その結果を RD2HTML クラスのオブジェクトでformatさせようとしています。ここで、formatの引数は3つのメソッドopenとclose、
writeを持つと仮定してます。前回みたようにメソッド名をフックにするというのはオブジェクト指向の典型的なやりかたでしょう。
なお、write の引数 arg に渡されるのは葉の各行を要素とする配列です。インライン処理はここで行なえます。1行で完結して入れ子になっていないように限定されたインライン処理は単に正規表現を考えて、
gsub! するだけですから宿題としましょう。
-- List6 rdohtml.rb: RDをHTMLのBASE要素の中身に変換
#!/usr/bin/env ruby
require "rdo/bparse"
class RD2HTML
include RDElements
MAP = {
Headline1 => "H1",
Headline2 => "H2",
Headline3 => "H3",
TextBlock => "P",
Verbatim => "PRE",
ItemList => "UL",
EnumList => "OL",
DescList => "DL",
ItemListItem => "LI",
EnumListItem => "LI",
DescListItem => nil,
Term => "DT",
Desc => "DD"}
def open(elm)
if MAP[elm]
print "<#{MAP[elm]}>"
end
end
def close(elm)
if MAP[elm]
print "</#{MAP[elm]}>\n"
end
end
def write(arg)
print arg.join
end
end
RDBlockParser.new(ARGF).parse.
format(RD2HTML.new)
さてすでに説明したブロック規則を実装したのが List 7〜12 です。少々長いので、いくつかのファイルに分割しています。
まず List 7 はコアとなる部分で、主要なメソッドparseを持った
RDBlockParserを定義しています。initializeは、引数に IO とタブ文字の幅を指定しています。その他に解析に必要な3つのスタックと、
List 3と同じ役割の @where、 そして結果を書き込む@treeを用意しています。
parseは慣用句「while gets」を使ったループで、各行毎にインデントが浅くなっていないかを調べてから、正規表現でブロックの種類を決定し、その処理をブロック要素名のメソッドで呼び出しています。もう少し詳しく見ると、インデントの深さはこれまでのBaselineをためてあるスタック
@baseのうちから、どこまでブロックを閉じる必要があるかを
find で探しています。つまり、インデントがこれまでのBaselineよりも浅いところpos) を探しているわけです。posが見つかればその深さまでのブロックをすべて閉じます。findのような良く使う処理が標準装備なのはうれしいことです。
parseの中でelseに続くVerbatimの例外的に処理メソッド
verbatim のなかでブロックを閉じる処理を行なっているため
redoでcaseを始めから再度評価しなおしています。
スタックで管理するのはインデント、Baseline、それから開いている余分なインデントの位置です。余分なインデントは、List系ブロックに許されています。これらのスタック操作は散在すると分かりにくくなりますから、ブロック要素を開閉するプライベートメソッドopenとcloseだけで操作するようにしています。openを呼び出している箇所はあとで掲げる各ブロック処理です。
ところで parse では textblock に対応するパターンの中で
base という関数を使っていますが、open では同名の引数を使っています。このような場合、まず変数が検索され、そのあとメソッドが検索されます。もしカブっている場合は、メソッドの方は ((self.base)) と書けば呼び出せます。ただし、プライベートメソッドでは「self.」などと陽にレシーバを指定できませんから、そういう場合は名前を工夫するのがよいのですが、場合によってはプロテクトメソッド*6にするのが適切かも知れません。
-- List7 rdo/bparse.rb: ブロック解析器のメインループとスタック操作
require "rdo/rdolib"
require "rdo/block"
class RDBlockParser
def initialize(f, tab = 8)
@file, @tab = f, tab
@tree = RDTree.new
@base = Stack.new
@block = Stack.new
@indent = Stack.new
@where = Ruby
end
def parse
while line = gets
unless /^\s*$/
i = indent(line)
pos = @base.find{|b| i<b}
if pos
close_upto(pos)
end
end
case line
when /^={1,3}\s\S/
headline(line)
when /^\s*\*\s*\S/
itemlistitem(line)
when /^\s*\(\d+\)\s*\S/
enumlistitem(line)
when /^\s*:\s*\S/
desclistitem(line)
when /^\s{#{base}}\S/
textblock(line)
when /^\s*$/
whiteline
else # i.e., Verbatim
verbatim(line)
redo # to ungets
end
end
close_all
@tree
end
def open(elm, base, i = nil)
i ||= base
@indent.push i
@block.push base
if base
@base.push base
end
@tree.open(elm)
if block_given?
yield
close
end
end
def close
@indent.pop
@base.pop if @block.pop
@tree.close
end
def close_all
until @tree.focus_root?
close
end
end
def close_upto(pos)
while @base.top and
@base.top >= pos
close
end
end
end
各ブロック要素に対応するメソッドは List 8 の rdo/block.rb にまとめました。TextBlockだけは空行を含めることが出来ないという規則があるので、その辺だけに気を使ってあります。ブロック要素のうちで、実際に印字されるデータを直接持つのは各Headline と TextBlock、Verbatim、それから
DescListItem 中の Term だけで、これらの要素を葉と呼ぶことにします。一方、ListListItem などはコンテナだと考えます。葉を open しているときだけは @tree.write で書き込むことが出来ます。
先にも述べた特別な存在である Verbatim ではメインループでの redo
に備えるため、呼び出し側である parse で最後に読んだ行オブジェクトを変数 swap という名前で保存しておき、Verbatim が閉じる時にはこの swap を Verbatim で最後に読んだ行 l で置き換えてから
break で脱出します。また Verbatim が終るのが分かった時点ではお尻に続く余分な空行を読んでいる可能性があるので、とりあえず読んだ行はスタックにためておき、余分な空行をすべて pop してから @tree に書き込んでいます。
-- List8 rdo/block.rb: 各ブロック要素の処理
require "rdo/rdolib"
require "rdo/const"
require "rdo/tree"
class RDBlockParser
private
def headline(l)
close_all
case l
when /^= (\S.*)/
open(Headline1, 0){
@tree.write $1
}
when /^== (\S.*)/
open(Headline2, 0){
@tree.write $1
}
when /^=== (\S.*)/
open(Headline3, 0){
@tree.write $1
}
end
end
def listopen(t, l)
if @tree.focus? TextBlock
close
end
if indent(l) == @indent.top
if @tree.focus? *LIST
unless @tree.focus? t
close
open(t, nil, indent(l))
end
else
open(t, nil, indent(l))
end
else
open(t, nil, indent(l))
end
end
def itemlistitem(l)
listopen(ItemList, l)
/^(\s*\*\s*)(\S.*)/ =~ l
i, text = $1.size, $2
open ItemListItem, i
open TextBlock, i
@tree.write text
end
def enumlistitem(l)
listopen(EnumList, l)
/^(\s*\(\d+\)\s*)(\S.*)/ =~ l
i, text = $1.size, $2
open EnumListItem, i
open TextBlock, i
@tree.write text
end
def desclistitem(l)
listopen(DescList, l)
/^(\s*:\s*)(\S.*)/ =~ l
i, text = $1.size, $2
open DescListItem, i
open(Term, i){
@tree.write text
}
open Desc, i
end
def textblock(l)
unless @tree.focus? TextBlock
open TextBlock, indent(l)
end
@tree.write l.sub(/^\s*/, "")
end
def whiteline
if @tree.focus? TextBlock
close
end
end
def verbatim(l)
if @tree.focus? TextBlock
close
end
swap = l
buf = Stack.new
i = indent(l)
open Verbatim, i
buf.push l.sub(" "*i, "")
while l = gets
unless /^\s*$/ =~ l
j = indent(l)
if @base.find{|b| j<b}
swap.replace(l)
break
end
end
buf.push l.sub(/^\s{#{i}}/, "")
end
while buf.top == /^\s*$/
buf.pop
end
buf.each{|i| @tree.write i }
close
end
end
そういえば List 3 はどこにいったのか思う人がいるかも知れません。それは
List 9 にあります。実は parse で使っていた gets は List 3
に手を加えたプライベートメソッドです。ちなみにふだん関数として使っている gets などのメソッドは組み込みのKernelモジュールで定義されたものですから同名のメソッドを再定義した場合は Kernel::gets と書けば呼び出すことが出来ます。タブ文字を適切な数のスペースに置換する方法は
[FAQ] に載っているものを使っています。
-- List9 rdo/rdolib.rb
require "rdo/util"
require "rdo/const"
class RDBlockParser
include RDElements
RD, Ruby = :RD, :Ruby
private
def gets
while line = @file.gets
case @where
when Ruby
case line
when /^=begin/
@where = RD
else
next
end
when RD
case line
when /^=end/
@where = Ruby
else
return tab2sp(line)
end
end
end
return nil
end
def tab2sp(line)
t = @tab
if t
1 while line.sub!(/\t/){
" " * (t - $~.begin(0)%t)
}
end
line
end
def indent(line)
/^(\s*)/.match(line)[1].size
end
def base
@base.top or 0
end
end
そういえば Stack などを紹介していませんでした。List 10 は、スタックと Module#mktoken を定義しています。みて分かるように Stack は
top というメソッドだけを追加した配列です。Rubyの配列は使いでがありますね。ついでにいえば、Array#shift と Array#push の組合せで配列をキューとしても使うことが出来ます。また、ここでは読み込む文書はEUCに限定しています。Rubyには標準でNKFライブラリがあるので、これでとくに困ることはありません。
-- List10 rdo/util.rb: スタックと定数定義関数
$KCODE = "EUC"
class Stack < Array
def top; self[-1]; end
end
class Module
def mktoken(*arg)
arg.each{|a|
const_set(a.to_s, a)
}
end
end
えっと、 mktoken はなにかというと、これはList 11で定義してある定数の定義のためです。List 3ではシンボルを直接書いていましたが、これでは分かりにくいバグの原因にもなるので定数としてまとめたわけです。こうして定数をモジュールにまとめておけば List 6 のようにクラスの取り込むことができます。定数定義自体は別に手で打ってもいいんですけど、識別子としてしか使わない定数の値を考えるのはなんだか面倒なので動的に処理しました。こういったコーディングの省略はマクロでやったり、いきなりevalを持ち出すことが多いのですが、Rubyでは動的なプログラミングのための道具が揃っているので、単一のモデルで考えることが出来て気持ち良いですね。
LISTはrdo/block.rbで使っています。一方、葉の集合を表すLEAFは次にみる rdo/tree.rb で使います。
-- List11 rdo/const.rb: 定数たち
require "rdo/util"
module RDElements
mktoken(:Headline1,
:Headline2,
:Headline3,
:TextBlock,
:Verbatim,
:ItemList,
:EnumList,
:DescList,
:ItemListItem,
:EnumListItem,
:DescListItem,
:Term,
:Desc)
LIST = [
ItemList, EnumList, DescList]
LEAF = [
Headline1, Headline2, Headline3,
TextBlock, Verbatim, Term]
end
List 12には parse で出てきた @tree.open、@tree.close、
@tree.write が実装してあります。また、木を育てている時に、いまどこを見ているかというのは@focus という変数に持っており、
@fstack は @focus のスタックです。この辺はまぁ見れば分かると思うんですが、説明が必要なのはおそらく format の周辺でしょう。
format は traverse を使って木をなぞり引数で与えられたフォーマッタ fmt のどのメソッドを呼び出すかを決定するための情報
act を Node#traverse からもらっています。Node#traverse は名前
(@name)が葉に属すのかどうかを調べ、属す場合はシンボル
:data と名前、文字列の配列をイテレータ引数に渡すことで
format のブロックが実行されます。葉でない場合はすなわち入れ子なので、@data の各要素に対してそのままブロックを渡して
traverse を実行しています。
当連載第2回でみたイテレータはこのように再帰構造に対しては絶大な威力を発揮します。
-- List12 rdo/tree.rb: 解析木
require "rdo/const"
class RDTree
include RDElements
Root = :Root
def initialize
@root = Node.new(Root)
@focus = @root
@fstack = Stack.new
end
def focus
@focus.name
end
def focus?(*arg)
arg.include? @focus.name
end
def focus_root?
focus == Root
end
def open(elm)
@fstack.push @focus
node = Node.new(elm)
@focus.data.push node
@focus = node
end
def close
@focus = @fstack.pop
end
def write(*args)
unless LEAF.include? focus
raise "non writable node `#{focus}'"
end
puts "write(", args, ")\n" if $DEBUG
@focus.push *args
end
def format(fmt)
traverse do |act, name, data|
case act
when :open
fmt.open(name)
when :close
fmt.close(name)
when :data
fmt.write(data)
end
end
end
private
def traverse(&blk)
@root.data.each do |node|
node.traverse(&blk)
end
end
class Node
include RDElements
def initialize(name, data = [])
@name, @data = name, data
end
attr_reader :name, :data
def push(*args)
@data.push(*args)
end
def traverse(&blk)
yield :open, @name, nil
if LEAF.include? @name
yield :data, @name, @data
else
@data.each do |node|
node.traverse(&blk)
end
end
yield :close, @name, nil
end
end
end
今回はRDの紹介しRDを理解するため比較的長めのプログラムをみてました。 Rubyには実際のプログラムを書いて初めてわかる便利さもいろいろあります。紙面の都合もあってあまり凝ったことはできませんでしたが、配列のいろいろな使い方や再帰的な構造でのイテレータ活用法などを挙げることができました。また、説明は省略しましたがさまざまなパターンマッチングを使っています。マニュアルやFAQを参照してみて下さい。
今回はToshさんのrdtoolやJARSを参考にしまくりました。心より感謝します。
``Ruby FAQ'', <URL:http://www.ruby-lang.org/ja/FAQ/rubyfaq-jp.html>
Tosh, ``Just Another RD Site'', <URL:http://www2.pos.to/~tosh/ruby/rdtool/ja/index.html>
Perl/Ruby Conference, <URL:http://perlruby-con.opensource.gr.jp/>
Rubyどうでしょう
著者: 後藤謙太郎
御意見,御感想,御批判の宛先: gotoken@notwork.org
*1 コンテナは容器のこと。具体的には配列やハッシュなど。
*2 人気のあるRD変換器。僕も愛用中
*3 Wnnのフロントエンドの一つ
*4 クラスの使用上の規模
*5 HTMLのA要素やLaTeXの\refのように別の部分を指し示す機能
*6 自分とサブクラスのインスタンスのみから呼び出せるメソッド