目次


洗練されたPerl: Perl 6文法と正規表現

Perl 6の文法と、Perl 5のParse::RecDescentモジュールでの正規表現を比較する

Comments

Perl 6プロジェクトはどんなPerlプログラマーにとっても熱い話題です。Perlは常に進化している言語であり、Perl 6は明らかに、考えられるほとんどすべての面でPerl 5から進化を遂げています(それでもやはり、同じものから由来していることは分かるのですが)。Perl 6はParrot上で実行します。ParrotはPerl 6のバイトコードのみならず、他の多くの言語もロードして解釈「できるはず」の多用途の仮想マシンです。

上の文で「できるはずの」と言ったのを心配する必要はありません。何ヶ月かかかって建設される建物を見たことがある人ならご存じでしょう。基礎が掘られた後、永遠と思えるくらい長い間、鉄の枠組みが立ったままになっているものです。作業員が出入りして、何事か作業は行われていますが、見かけは相変わらず、あの見苦しい錆びた鉄の枠組のままです。そう思っていると突然、数日のうちに建物が完成してしまうのです。Perl 6プロジェクトはこの長い中間フェーズにあり、錆びた鉄の枠組みという外観の奥深くで、作業員が働き続けています。このプロジェクトの進行状況を見たいと思うのでしたら、最新のParrotリリースを試してみて、Perl 6の週刊アップデート情報を見てください(参考文献にリンクがあります)。

この記事では、Perl 5で現在使用できるParse::RecDescentモジュールと比較しながら、皆さんをPerl 6の文法と正規表現の案内ツアーにご案内します。Perl 5を事前に知っていてParse::RecDescentに少し慣れており、字句解析や構文解析の経験があれば、この記事を読む上で大いに助けになるでしょう。しかしこの記事は、Perl 6の文法と正規表現に興味を持つPerlプログラマーであれば、どんな人にも役に立つように書いたつもりです。

Perl 6の正規表現と文法の概要

まず一つ、最初に言っておくべきことがあります。Perl 6は:p5修飾子を使うことによって、Perl 5の正規表現をサポートします。これはPerl 6の正規表現に興味のない人やPerl 6の正規表現に変換したくない人には喜ばしいことです。それに、Perl 6の正規表現はPerl 5の正規表現と大幅に違っている可能性はありますが、実際に大幅に違っているはずはないのです。

Perl 6の正規表現は、必要な場合には再利用することができます。一つの単語に一致する正規表現を再利用することは馬鹿げていますが、設定ファイルを構文解析する時には、(設定の構文の複雑さ、その構文がどのくらい頻繁に変化するか、などによっては)正規表現の再利用はほとんど必須です。

既にRegexp::Commonモジュール(参考文献)はPerl 5で正規表現を再利用しようとしていますが、Perl 5では正規表現の再利用は許していないので、このモジュールはモジュール・インターフェースに隠蔽されています。Perl 6では最初から、この再利用を許しているのです。

Perl 5の正規表現のような、読みにくい、高密度なPerl 6正規表現を書くこともできますが、空白コメントはデフォルトでオンにされます。ですからPerl 5では「hello there」を「hello there」自体と一致させることができますが、Perl 6ではちょっと違って、/hello <sp> there/ を要求する必要があります。これによって、正規表現では、用語の明確な分離ができるようになります。

もっと重要なことは、Perl 6の正規表現は、文法内部のルールで使われた場合には、必然的に密度は低くなるということです。プログラマーであれば、リスト1よりもリスト2の方が、理解も維持管理もずっと簡単だと思うでしょう(私はそうあって欲しいと思いますし、Larry Wallも同じ思いです)。

リスト1. 文法なしの正規表現
# note this is just a language example, not an accurate name matcher
# Perl 6 <[A-Z]> is equivalent to the Perl 5 [A-Z]
# Perl 6 :w modifier surrounds all tokens with "automagic" whitespace,
# which basically means it will match what most people would call
# "words"
$name = m:w/ <[A-Z]><[a-z]>+ <[A-Z]><[a-z]>+ /;
リスト2. 文法内部のルールとしての正規表現
# note this is just a language example, not an accurate name matcher
grammar English
{
 rule name :w { <singlename> <singlename> };
 rule singlename { <[A-Z]><[a-z]>+ };
};

リスト2はずっと読みやすいだけではなく、維持管理も、より簡単です。例えばPerl 6では<upper>ルールと<lower>ルールが既に定義されているので、色々なことが楽になります。

リスト3. 文法内部のルールとしての正規表現、改善版
# note this is just a language example, not an accurate name matcher
grammar Names
{
 rule name :w { <singlename> <singlename> };
 rule singlename { <upper><lower>+ };
};

これでもう、できあがりです!<upper><lower>を使った時に、正にコードの再利用をしたのです。さらに、これまで名前はAからZまでで始まるものに厳しく制限されていましたが、今度はUnicode名も扱うことができます。コード再利用は素晴らしいのです。

例えば(Don Quixote de la Manchaなど)人の名前やその他の名前におけるダッシュの修正など、ほとんど間違いなく、さらなる手直しが必要でしょう。ここでも、一つのルールに対する変更を分離したり、必要な場合に新しいルールを作ったりすることがどれほど容易か、分かって頂けると思います。

文法というのは、ごく単純な概念です。文法というのはプライベートな名前空間とプライベートなサブルーチンを持つパッケージで、各サブルーチンがルールと呼ばれます。文法は他の文法から継承することができます。これによってプログラマーは、他の人のコードを再利用することも、再利用可能なコードを書くことも両方できるようになります。そうした再利用の価値は、Perlモジュールに対するCPANアーカイブの成功から、明白と言うことができます。Perl 6文法はルールで正規表現を使用します。そうするとこうしたルールは、他のルールの内部で使用できるようになります。

Parse::RecDescentをPerl 6文法と比較する

Parse::RecDescentを知っている人であれば、これが強力なツールであることを知っています。これは、ほんの少しのコードで強力な文法を生成する、Perl 5モジュールです。こうした文法はPerl 6の文法とどのくらい似ているのでしょうか。ご存じかも知れませんが、Parse::RecDescentを書いた人はDamian Conwayであり、Perl 6にも深く関わっている人です。Parse::RecDescentでうまく動作することが示された考え方の幾つかが、Perl 6に採り入れられたのは驚くには当たりません。幾つかの構文もPerl 6に入り込んでいます。

Parse::RecDescent(以下、P::RDと呼びます)はnew() モジュール・メソッドを使って新しい文法を作ります。各P::RD文法は、P::RDクラスにブレスされたオブジェクトになり、文法中の各ルールはメソッドとして、アクションを起こすために使用します。P::RD文法は構文解析プロセスに統合された一部として、それぞれのルールにアクションを関連付けます。これは非常に重要なことです。Perl 5では構文解析は自分自身へのイベントであり、アクションは、大都会に至る道すがら、遠くからでも猫を混乱させることが証明されている拡張構文を使って轢き殺した、路上轢死体のようなものです。この違いにより、一致が検出された時に何かが起こるようにするという点で、P::RDはPerl 5の正規表現よりもずっと効果的なものになります。

Perl 6プログラマーは、アクションは有用なものであるという、P::RDの教訓を学びました。そして今やアクションは一級の市民としての地位を確立したのです。一致が発見されればどこであっても、アクション(コード・ブロック)が実行されます。一致の相手の内容まで変更されるのです! しかもこうしたアクションに対する構文は、P::RDでの構文と同じくらい単純なのです。

リスト4. アクションを伴うParse::RecDescent文法
# small extract from my cfperl.pl program's global parser
my $parse_global = new Parse::RecDescent (q{
  input:  blank | comment | class | section
  comment: /^\s*/ '#' { 1; }
  blank: /^\s*$/ { 1; }
  section: /\w+/ ':'
   { $::current_section = $item[1];
     $::current_classes = 'any'; 1;
   }
  class: compound_class '::'
   { $::current_classes = $item{compound_class}; 1; }
  compound_class: /[-!.|\w]+/
});
$parse_global->input("TEXT GOES HERE");

この文法は一つだけ、inputというルールを持ち、これはblankcommentあるいはsectionのどれかに一致します。これらのルールはそれぞれ独立の定義、あるいは他のルールに基づく定義、あるいはその両方を持ちます。

アクションは、普通のコード・ブロックのように、{ } 括弧で囲まれることに注意してください。このアクションはセクションに対して、グローバル変数$current_sectionをちょうど今一致したメソッドにセットし、$current_classesグローバル変数をリセットします。またこのアクションはクラスに対しては、グローバルな$current_classes変数を、一致した項目にセットします。

これはPerl 6ではどう見えるのでしょう。

リスト5. リスト4の文法をPerl 6に変換する
# this may be buggy - it's certainly untested
# every input is known to be one line, without newline characters
grammar Global
{
 rule input { <blank> | <comment> | <class> | <section> }
 rule blank { ^^ \s* $$ }
 rule comment { ^^ \s* \# }
 rule section
 { (\w+) \s* \:
  {
   $::current_section = $1;
   $::current_classes = 'any';
  }
 }
 rule class { (<compound_class>) \s* \:\:
  {
   $::current_classes = $1;
  }
 }
 rule compound_class { <[-!.|\w]>+ }
}

Perl 5の正規表現

Perl 5の正規表現をよく知っている方はこのセクションを飛ばしてください。

Perl 5の正規表現はPerl 5にとってはお馴染みのものです。Perl 5の正規表現では、一致の場合にはm//演算子でマークを付け(オプションの場合もあります)、置換の場合にはs/// 演算子でマークを付けます。/ という文字は、他の記号、具体的に正規表現のような特別な操作子で置き換えることが可能ですが、全く正規表現と同じようなもの(例えばtr/// のようなもの)ではありません。Perl 5の正規表現のポイントは、「これを検索しなさい」または「これを検索してあれと変換しなさい」のいずれかを言うだけだ、ということです。

簡単に見えますよね? 普通は確かに簡単です。正規表現には、表現の内側にも外側にも修飾子があります。例えば大文字小文字の区別、先頭の一致、複数の一致、空白の無視、一致の個数などの修飾子です。スピードアップのために正規表現をプリコンパイルすることさえできます

もっと複雑なオプションは無視して、正規表現の基本的な構文だけを見てみましょう。次の例を見てください。

リスト6. Perl 5の正規表現の例
# 1: look for "color"
# matches "green and red are colors"
m/color/
# 2: look for "color" at the beginning of the line
# matches "color me blue" but not "this is my color"
m/^color/
# 3: look for "frog" followed by anything, followed by "jump"
# matches "the frog jumped" but not "jump, you frog"
m/frog.*jump/
# 4: look for a numeric digit, then 1 or more spaces, then another digit
# matches "671 2" but not "numbers 444, 222"
m/\d\s+\d/
# 5: save the first number seen (multiple digits)
# matches AND returns "46755332" but not "how do you do?"
m/(\d+)/
# 6: replace "wall" with "plaster" everywhere
s/wall/plaster/g
# 7: replace the FIRST number seen with N
s/\d+/N/

最初に気がつくのは、こうした正規表現がすべて一行にあるということです。Perl 5の正規表現は複数の行にまたがることができ、コメントを含むこともできるのですが、大部分のプログラマーは実際にそれをしようとはしません。

複数行やコメントが使えるとしても、Perl 5の正規表現は高密度だとは言い難いものです。初心者のPerlプログラマーにとっては、複数行やコメントはむしろ邪魔に思えるものです。ところがその密度の中に豊富な情報が隠されているのです。Damian ConwayはPerl 5の正規表現を、「不可思議で、異様、一貫性に欠け、曖昧」と称しており、初心者の意見に同調しているようです。しかし私の意見では、Perl 5の正規表現の密度というのは、Perlがなぜそれほどまで強力であるかを示す理由の一つなのです。ただし正規表現の維持管理はかなり困難なもの、ということだと思います。Perl 6の課題は、この強力さを保ったまま構文を整理することです。

私が読みやすさのことばかり言っていると思われるかも知れませんが、それには理由があります。新人のPerlプログラマーは、Perl 5の正規表現に恐れをなしてしまうのです。私はニュースグループやメーリング・リストで何度もそれを見てきました。言語の機能であるはずのものが、その言語のユーザーを恐れさせるようになったら、何かを変えるべき時なのです。

Perl 5の正規表現に欠けているのは読みやすさだけではなく、構造にも再利用性にも欠けています。こうした属性は一般的に、もっと上位のプログラミング構造体に関連付けられるものです。これから先では、Perl 6の正規表現ではこうした3つの問題をどのように扱っているのかを見て行きましょう。でも主なものは、やはり読みやすさなのです。

Perl 6の構文解析と字句解析

Perl 5には組み込みの構文解析機構が何もありませんが、字句解析機構は正規表現を通して提供しています。

構文解析と字句解析に関してちょっと説明しておきましょう。字句解析(lexing)の定義を一つ挙げると、入力を意味のある単語(lexing tokensとかlexing lexemesとも呼ばれます)に分割することと言えます。特定な実装により、それ以上であることも、それ以下であることもありますが、一般的な概念として、あるプログラムをプレーン・テキストとして与えると、そのテキストの中にあって意味をなす各単語の目的と境界を、字句解析が教えてくれるのです。

正規表現は単純な固定フィールドから複雑なネスト・パターンまで、ある範囲の構文解析パターンをカプセル化します。字句解析は必ずしも正規表現で行う必要はありませんが、プログラミング言語やデータ言語のジャングルの中を通り抜けるには、正規表現がちょうど手頃なのです。人が固定フォーマットのテキストでプログラムやデータを書くのであれば字句解析は非常に簡単でしょうが、私達は固定フォーマットで書いたりはしないのです。

字句解析が終了すると、意味のないデータ・ストリームが、パーサーにとって意味のある単語シーケンスに変換されます。パーサーというのは、こうした意味のある単語群を取り入れ、そこから単語の型や目的を認識することによって構文解析ツリーを構築するソフトウェアです。

これは実際、私達自身が言語の知識として持っている概念を非常に単純に表現したものです。字句解析は私達が持っている言語知識の一部であって、「これは文である。これは句読点である。23は一つの単語である。」というように解釈します。構文解析も言語知識であって「この文には動詞があり、主語があり、幾つか形容詞があり、代名詞がある」などと言うのです。構文解析が終了すると、(コンピューターにとって)意味のないデータ・ストリームが、コンピューターが理解できる何か、となるのです。

下記は、ある文の字句解析と構文解析の例です。これは適当に作ったものですから、真剣に分析しようなどとは思わないでください。それよりもむしろ、各解析レイヤーでの責任の分離がどうなっているかを見てください。

リスト7. 字句解析と構文解析
Sentence:
The sky is blue, wow!
Lexer: [The] [sky] [is] [blue] [%%comma%%] [wow] [%%exclammation%%]
(Note how the punctuation was inserted with special symbols.)
Parser:
Declarative Sentence =
 Specific_Subject + Verb + Adjective + Optional_Exclammation =
  [The sky]         [is]     [blue]          [, wow!]

この例の構文解析は、宣言の具体的な内容に関しては、文と深く関わっている訳ではないので気にしません。ただし、主語が限定的か(「the sky」)そうではないか(「sky」)に関しては非常に注意を払います。これはその違いによって、文の意味的な内容が大幅に変わってくるためです。

Parse::RecDescentモジュールは字句解析をスムースに行うために、Perl 5の正規表現を使っており(P::RDルールが一致に他のルールを使っていない時には、まず確実にそれが字句解析ルールだと言うことができます)、その上に構文解析機構を構築しています。ですからPerl 5の上にP::RDがあるというのは、構文解析と字句解析の強力な組み合わせなのです。

先に述べた通り、Perl 5とP::RD、そしてPerl 6の間には深いつながりがあります。Perl 6での構文解析や字句解析が、上で説明したPerl 5とP::RDの組み合わせと非常に似た方式で行われるのは驚くには当たりません。Perl 6の正規表現は改善されており、他の正規表現も含むことができるので、文法無しであっても再利用可能になっています。字句解析の面からこれを見ると、例えば整数の字句解析定義を、実数や分数の字句解析定義を作るために使うことができることを意味します。

Perl 6の正規表現の上には、Perl 6の正規表現と緊密に統合されたPerl 6文法があります。P::RDと同じように、こうした文法は簡単なルールを使って字句解析を行い、その後でもっと複雑なルールを使って字句解析された入力を構文解析します。ですからPerl 6の文法と正規表現は、Perlコミュニティーにおける字句解析と構文解析に対する要求に完全に答えるものであり、Perl 5でP::RDモジュールを使った場合に見られるような、実績のある手法に基づいているのです。

その他のPerl 6の正規表現と文法に関する豆知識

Perl 6の正規表現と文法には他にも、Perl 5やP::RDでは得られない面白い特徴があります。

コミット・ディレクティブは構文解析の最適化に非常に便利です。構文解析プロセスのある点に達した時に、コミット・ディレクティブは、現在のルール以外のものは何も一致しない、と言うのです。例えばある言語で「color」の後に続く単語が「blue」のみであったとすると、この文法は(擬似コードで)color commit() blueを規定し、「color red」は構文解析しなくなります。Perl 6文法にはコミット・ディレクティブがあり、現在の単語、置換文字、文法ルール、一致演算子に至るまでの全てが、一致の後はバックトラックされることがないことを規定します。一つのレベルのコミットを提供するだけのP::RDには、これだけきめ細かな制御はありません。大きな文法に対しては、これは非常に便利な機能です。これがややこしいと思われる人は、Perl 6では現在の一致に対して、いつ、どういうレベルでコミットすべきかを、皆さん自分が決めることができる、ということだけを覚えておいてください。

コミット・ディレクティブに対応して、failと呼ばれるものがあります。failを使うと、論理条件によってPerl 6文法ルールをフェールさせることができます。月の中で無効な日を排除するためにfailを使うルールは(実際の月を考慮しなければ)、rule date {:w <month> (\d+): { fail if $1 > 31 } }のようになります。(\d+) の後の: が、もし「32」がフェールしたらバックトラックせず「3」を試すべきだ、とPerl 6に対して言うのです。

Perl 6の文法や正規表現では非グループ化一致(non-grouping matches)を許しており、これは結果を保存しません。Perl 5では、正規表現m/(a)(b|c)(d)/ は最初に「a」を戻し、次に「b」または「c」を、そして次に「d」を戻します。もし「b」と「c」を気にしないとしたらどうでしょう。Perl 5では困難なことに、ちょっと異様な、しかも常に使用できるとは限らない正規表現、?: 修飾子を使わない限り、代替(alternation)を無視することができませんでした。Perl 6では非グループ化一致規定子[b|c] を使うことができるので、「b」と「c」は保存されません。これは最適化の補助として貴重なものです。

Perl 6の正規表現では、Perl 5では簡単に実現できなかった、「これがN回目に起こる時を捉える」ことを容易に規定することができるのです。

Perl 6のUnicodeサポートは、正規表現に関してはPerl 5よりもずっと良くなっています。

一時的に文法を表現できる-ローカル変数は、文法アクションの中で定義、設定することができます。これらは一致が成功した場合にのみ値を持つ、仮の変数です。

さらにその他にもまだまだあります。

まとめ

参考文献に挙げたPerl 6のオンライン・リソースをよく調べてください。Perl 6は進行中のプロジェクトなので、これらはどれも重要なものです。

Perl 5で既にParse::RecDescentを使っている人であれば、正規表現と文法に関してPerl 6で変わった内容の違いは、主なバージョン番号変更の時にありがちな違いよりもずっと小さいことがよく分かっているでしょう。現在P::RDを使っていない人にとっては、P::RDが学ぶだけの価値のあるツールであることが、この記事で分かって頂けたと思います。皆さんの興味がむしろPerl 6の方にあったとしても、Perl 6の文法機能とP::RDは非常に多くの共通点を持っているので、P::RDを学ぶだけの意味はあるのです。

最後に、皆さんが私と同じようにPerl 6の機能に興味を持ってくださること、そこからPerl 6プロジェクトを注意深く見守り、場合によっては何か貢献することまで考えてくれることを期待しています。Perl 6はコミュニティーの努力としてPerl 5を書き直したものです、つまり皆さんや私のような人が書いたものなのです。ですから、まだコミュニティーに加わっていない人には、ぜひ参加して欲しいと思っています。

この記事を丁寧に見てくださったDamian ConwayとLuke Palmerに深く感謝します。


ダウンロード可能なリソース


関連トピック

  • Parrotのメイン・ページを(そして今はPerl 6を)見てください。
  • Perl 6の完全なチュートリアルに関しては、Perl 6 and Parrot Essentialsを読んでください(2004年O'Reilly & Associates刊)。
  • Parse::RecDescent CPAN moduleのホームページは、このモジュールの背景を知るために訪れるべきサイトです。
  • Perlの生みの親、Larry WallによるPerl 6の正規表現に関する案がここにありますので、読んでみてください。
  • その後で、Allison RandallとDamian Conwayによる、Larry Wallの記事の解説がここにありますので、読んでみてください。
  • Damian ConwayがPerl 6の正規表現の実際例を書いていますので、これを読んでみてください。
  • The Perl 6 weekly updatesで最新情報を知ってください。
  • CPANにあるRegexp::Common moduleを読んでみてください。
  • CPANのPerl6::Rules CPAN module(Perl 5でのPerl 6の正規表現構文)をよく調べてください。
  • Parse::RecDescentを使って簡単なコマンドライン・インターフェースを作る方法を、TedがWriting Perl programs that speak Englishで説明しています(developerWorks, 2000年8月)。
  • Parsing with Perl modules (developerWorks, 2000年4月) の中で、Tedがテキスト構文解析に関するCPANモジュールの概要を説明しています。
  • developerWorksでTedが書いている、洗練されたPerlシリーズのPerlに関する全記事を読んでください。
  • developerWorksのLinuxゾーンにはLinux開発者用の資料が他にも豊富に用意されています。
  • Developer BookstoreのLinuxセクションではLinux関連の書籍が値引きして購入できますのでご利用ください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=228121
ArticleTitle=洗練されたPerl: Perl 6文法と正規表現
publish-date=11022004