こんな記事を読んだ。
正規表現はメンテ不可能になりがちなので、キャプチャグループに名前を付けたり、キャプチャさせなかったりしてメモリを節約するのは良いこと。
Scalaでも同じことができそうだったけれどはっきりとは覚えていなかったので復習した。
名前付きキャプチャグループ
Scalaにはregex literalがないので.r
メソッドでStringをRegexに変換して使う。
正規表現では、改行やバックスラッシュを含むような表現が頻出するため、"""string"""
やraw"string"
といった記法(前者は改行が可能)を使って文字列リテラルを定義すると便利だから覚えておこう。
val Re = """v(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)""".r Re.findFirstMatchIn("v1.2.3") match { case Some(mtch) => mtch.group("major") case None => ??? }
正規表現を文字列にマッチさせ、マッチオブジェクトを取り出すにはRegex.findFirstMatchIn
を使う。
そして、文字列に該当するマッチグループの文字列を取り出すにはMatch.group()
を使う。
ちなみに、名前付きキャプチャグループを使わない場合、Scalaでは次のように書いたほうが便利だ:
val Re = """v(\d+)\.(\d+)\.(\d+)""".r "v1.2.3" match { case Re(major, minor, patch) => ??? }
このように書けるのは、Regexはunapply
を実装しているので、文字列をRegexにmatch
させるといった表現ができるためだ。残念ながらこの方法ではMatch
オブジェクト自体は取り出せない。
後方参照
後方参照もScalaで可能だ。
val SameRe = """v(?<version>\d+)\.\k<version>\.\k<version>""".r "v7.7.7" match { case SameRe(_:_*) => "matched" }
case SameRe(_:_*)
と書いているのは、いくつマッチしてもその結果を捨てるという意味。数が合わなくて実行時エラーになるのを防いでいる。
非キャプチャグループ
非キャプチャグループもScalaで動作する。
val HelloWorldRe = """(?:Hello|Goodbye), world!""".r "Hello, world!" match { case HelloWorldRe(_:_*) => "matched" }
元記事ではregexに^
と$
を付けていたが、Scalaのregexはデフォルトでanchorされているので^$は使わなくてよい(必要に応じて.unanchoredする)。
まとめ
Scalaでも名前付きキャプチャグループや非キャプチャグループを使うことができることがわかった。また、一定の場合にはmatch
構文と組み合わせると便利なこともわかった。
matchと名前付きキャプチャグループの相性が悪いので、なんか良い方法がないだろうかと思う。また、リテラルでそのまま正規表現を書いている場合はコンパイル時にキャプチャグループの名前は判明するはずなので、コンパイラが支援して自動的に名前付きキャプチャグループを安全に利用できるといいのにと思う。
ちなみにESLintのprefer-named-capture-group
にあたるlinterは見当らなかった。wartremoverとかで作ってみるのも面白いと思う。