Scalaの正規表現

togetter 正規表現が構文として必要かどうかという話から
プログラミング言語における正規表現リテラルの必要性について

こういう収集がつかなそうな話題にあまり首突っ込むの好きじゃないんですが、blogに書いておけば、まぁそれはそれでScalaをあまり知らない人にとっては役に立つだろうから、丁寧に説明しておきましょう。


togetter(と、その他関連するtweet)はあまり読んでません。
とりあえずkazuhoさんがわかりやすくblogに要点まとめているので、まずそれに対応するかたちで説明しましょう。


また、大前提としてScalaに構文としての正規表現リテラルはありません。なので、以下の説明を読んで
「いや、それは単に苦しい言い訳だし、やはり正規表現リテラルは存在したほうがいいでしょ」
と思う人もいれば
「なるほど、このくらいの機能があれば、たしかにそれほど正規表現リテラル必要ないな」
と思う人もいて、人それぞれでしょうが、そこにはあまり感知しないというか、とにかくScalaの現状を説明しましょう。




kazuhoさんは、まず最初に3点あげています。

  • より完結なコード
    • (細かく分けると、オブジェクトの生成の構文と、エスケープの件について2点挙げてる)
  • エラー検出タイミング
  • 実行速度

それぞれ見ていきましょう。



これは、Scalaの場合 "abc".r という、rメソッド呼び出すだけです。まぁ2文字増えるので、そこを許容するかどうかです。(ただしダブルクォート3つ使うraw記法使うと、合計6文字増えてしまいますが・・・)しかし、それより重要なのが、Rubyなどと違い、現状のScalaでは以下のようにmatch式のcaseのところに直接書いてそのままmatchさせることができません

"abc" match {
  case "abc".r => // matchした場合の処理。これは現状のScalaで無理。一旦正規表現オブジェクトを変数に入れる必要がある
  case _ => // matchしなかった場合
}

マクロとかで将来的に解決するのかもしれません(?)が、この点は明らかに面倒です。



エスケープの件は、ダブルクォート3つで囲うという、そういう機能があるから問題ないでしょう。



  • エラー検出タイミング

Scala2.10以降、マクロ使えばコンパイル時に可能です。
http://d.hatena.ne.jp/xuwei/20130220/1361323201
https://gist.github.com/eed3si9n/4992129
ただ、標準ライブラリには入ってません。
(標準ライブラリに入ってないことも相まって)実際「絶対コンパイル時にチェックしたい」と思う人はそれほど多くない(?)らしく、わざわざマクロ使ってコンパイル時チェックしてる人はあまり見かけませんね・・・。



  • 実行速度

正規表現リテラルがあれば、正規表現を言語処理系側でコンパイルして使い回すことができるので、実行速度が向上」

これは、速度を計測してみないとなんともいえませんが。まず、現状では(自然にその場で書いた場合に)勝手に使いまわすような仕組みを入れるのは、誰もやってないでしょうね。

ただ、どこか一箇所にMap[String, Regex]型のキャッシュ用オブジェクトを保持しておいて、正規表現生成のメソッド呼び出し時に
「作られていたらキャッシュから取得、なかったら新規作成」
というような単純な実装でいいなら、とりあえず同じ仕組みは不可能ではないでしょう。が、Mapにアクセスしたりする他の時間がかかるようになるので、とりあえずそんな実装で同じ仕組み(?)を実現しても効果あるのかどうかわかりません・・・誰かやってみて計測して・・・。
あとは、マクロでコンパイル時に最適解の可能性もあるかも?
まぁ今後も「正規表現オブジェクト生成の速度」は特別に問題視されたりはしないでしょう。*1少なくとも標準ライブラリにそういう仕組みが入ることはなさそうです。




あと、kazuhoさんのblogでは触れられない?ですが、(twitter上では焦点の一つになってた?)変数埋め込みもScala2.10から可能です。





さて、ここからScala独自の話です。Scalaには以下のように正規表現オブジェクトがExtractor*2にもなるという機能があります

Welcome to Scala version 2.10.3 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_45).
Type in expressions to have them evaluated.
Type :help for more information.

scala> val digitAnd = """(\d+).*""".r
digitAnd: scala.util.matching.Regex = (\d+).*

scala> "123aaa" match {
     |    case digitAnd(n) => Some(n)
     |    case _ => None
     | }
res0: Option[String] = Some(123)

括弧でグループ化しておいたところのみが抽出されます。グループの数はいくつあってもOKなので、3つグループがあれば、3つの変数が同時にバインドできて便利です。
さて標準ライブラリの範囲内だと、抽出されるグループの数のチェックは実行時なので、例えば以下のようにdigitAnd(n, m)と書いてもコンパイルは通っていまいます。

scala> "123aaa" match {
     |    case digitAnd(n,m) => Some((n, m)) //digitAndは2つmatchするはずがないから、ここは通らないことがコンパイル時に分かるはずだけど現状ではコンパイル通ってしまう
     |    case _ => None
     | }
res2: Option[String] = None


これらをマクロ使って、以下のようにすることが2.10時点でも可能だと思うのです

scala> val digitAnd = """(\d+).*""".r //マクロ呼出し
digitAnd: scala.util.matching.Regex = (\d+).*

scala> "123aaa" match {
     |    // case digitAnd(n,m) => Some((n, m)) // これはコンパイルエラー
     |    case digitAnd(n) => Some(n) // コンパイル時にマクロで正規表現解析したので、Intになると分かる
     |    case _ => None
     | }
res2: Option[Int] = 123

このように
コンパイル時に正規表現解析して、それによりExtractorの型が自動で適切に決まる!」
という機能あったら便利というか、これは他の言語にあまり真似できない感あってすごいと思うんですが。
誰か作ってくれませんかね?



あとは、関数型勢からすると「正規表現よりパサー(ry」とか「正規表現ないことにより、逆に使いすぎないメリットが(ry」とか「正規表現は合成可能じゃないから(ry」とかありますが、そこは話しても収集つかなそうなのでやめておきます。
あと、ちなみにScalaにはRegexパーサーというものがありますが、それも少し話逸れるというか、書くの面倒になったので略

*1:他に優先してやるべきことがある

*2:unapplyやunapplySeqという名前のメソッドで、Option型を返すものを定義しておくと、パターンマッチに使えるという機能のこと