15
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

HaskellAdvent Calendar 2015

Day 11

lens-regex で正規表現を便利に

Last updated at Posted at 2015-12-11

寝るまでは今日! UTC ではまだ 11 日!
なお当初の予定では travis-ci で stack を使うと便利みたいな話を書こうと思っていましたが、thimura は寝てしまったのでお蔵入しました。

Haskell で正規表現

みなさんは Haskell で正規表現を使っていますか?

Haskell を使い始めた頃は、他のプログラム言語では正規表現に親しんでいることもあって正規表現を使おうとする場合が多い気がします。でも、ある程度 Haskell に慣れてくると、パーサーコンビネータの方が便利とか言い出して、あまり積極的には使わなくなる人が多いのではないでしょうか。

加えて、Haskell の正規表現の演算子 =~ は返り値によるオーバーロードをしていて、普通にプログラムの中で使う分には返り値の型が推論されるのであまり問題ありませんが、ghci を電卓代わりに使う人たちにとっては、いちいち返り値の型を指定してやらないといけないので、かなり面倒です。

返り値の型も、Bool だったり Int だったり [[String]] だったり、はたまた (AllMatches (Array Int) MatchArray) だったりとかで正直覚え切れません1

そこで今回は、ちょっと多相すぎて使いづらい =~ のかわりに、Lens で扱える正規表現ライブラリがあれば便利に違いないと半年くらい前にやっつけ実装した lens-regex というライブラリを紹介します。

前提知識

あぁ ^~ Lens の演算子 ^? くらいは分かるんじゃぁあ ^~

インストール

普通に cabal とかでインストールしてください。

{.shell}
$ cabal install lens-regex

必要に応じて、regex-posixregex-pcre などのバックエンドもインストールしておいてください。

使い方 (下準備)

まずはじめに、必要なモジュールの import と言語拡張の有効化をしておきます。

{.haskell}
{-# LANGUAGE QuasiQuotes #-}
import Control.Lens
import Text.Regex.Posix

import Text.Regex.Lens
import Text.Regex.Quote

Text.Regex.Quote - 正規表現リテラルもどき

Haskell に正規表現リテラルはないため、ダブルクォートやバックスラッシュが含まれる正規表現を書こうとするとバックスラッシュ地獄になりがちです。Text.Regex.Quote は正規表現リテラルのかわりに QuasiQuote を使って正規表現リテラルモドキを提供します。例えば

[r|[^"]*|]

と書くと

{.haskell}
(makeRegex "[^\"]*") :: Regex

のように書いたのと同じ意味になります。

めざとい人は、ここで :: Regex という型注釈が付いていることに気がついたかもしれません。この Regex 型は、QuasiQuote と同じ翻訳単位にある Regex 型を指しており、Haskell の正規表現バックエンドは、それぞれのモジュールで同名の Regex 型を提供しているので、Text.Regex.Posix のかわりに Text.Regex.PCRE を import すれば POSIX バックエンドのかわりに PCRE バックエンドが利用できるようになります。

なお、|] がパターン中にあると QuasiQuote の終了と解釈されてしまうため、その場合は普通の文字列リテラルでがんばってください。ちなみに昨日 PukiWiki のテーブルを置換する際 [^|] と書こうとして、とてもつらい思いをしました。つらい。

また、現時点では式展開にはまだ対応できていません。気が向いたらがんばります。

使い方

では実際の lens-regex の使い方を紹介します。

文字列を走査し、マッチした最初の文字列を返す

{.haskell}
> "abc adc abbc def hoge asdc" ^? regex [r|a[^ ]*c|] . matchedString
Just "abc"

matchedString を付けない場合は MatchPart 型の値が返ります:

{.haskell}
> "abc adc abbc def hoge asdc" ^? regex [r|a[^ ]*c|]
Just (MatchPart {_matchedString = "abc", _captures = []})

matchedString とかいう名前は長すぎる上に、いまいち分かりにくいので変えたい……

文字列を走査し、マッチした文字列のリストを返す

regexIndexedTraversal として定義されているので、^? のかわりに ^.. を使うと、マッチした全ての文字列のリストを得ることができます:

{.haskell}
> "abc adc abbc def hoge asdc" ^.. regex [r|a[^ ]*c|] . matchedString
["abc","adc","abbc","asdc"]

マッチした部分を置換する

regex は setter としても使えます。

{.haskell}
> "irotoridori no sekai" & regex [r|s.*i|] . matchedString .~ "hikari"
"irotoridori no hikari"

> "abc adc abbc def hoge asdc" & regex [r|a[^ ]*c|] . matchedString .~ "nya"
"nya nya nya def hoge nya"

もちろん %~ を使うこともできます:

{.haskell}
> "abc adc abbc def hoge asdc" & regex [r|a[^ ]*c|] . matchedString %~ (\s -> "[" ++ s ++ "]")
"[abc] [adc] [abbc] def hoge [asdc]"

文字列を走査し、マッチした文字列のうち n 番目を返す。無ければ Nothing を返す

regex は IndexedTraversal なので以下略

{.haskell}
> "abc adc abbc def hoge asdc" ^? regex [r|a[^ ]*c|] . index 0 . matchedString
Just "abc"
> "abc adc abbc def hoge asdc" ^? regex [r|a[^ ]*c|] . index 2 . matchedString
Just "abbc"
> "abc adc abbc def hoge asdc" ^? regex [r|a[^ ]*c|] . index 4 . matchedString
Nothing

文字列を走査し、マッチした文字列のうち n 番目を置換する

{.haskell}
> "abc adc abbc def hoge asdc" & regex [r|a[^ ]*c|] . index 2 . matchedString .~ "NYA"
"abc adc NYA def hoge asdc"

> "abc adc abbc def hoge asdc" & regex [r|a[^ ]*c|] . index 1 . matchedString %~ map toUpper
"abc ADC abbc def hoge asdc"

> "abc adc abbc def hoge asdc" & regex [r|a[^ ]*c|] . index 1 . matchedString . traversed . from enum %~ (+1)
"abc bed abbc def hoge asdc"

後方参照を使う

MatchPart には matchedString の他に captures があり、() でグルーピングした部分を参照することができます。

> "<h1>shinku</h1>kawaii" ^? regex [r|<([^>]*)>([^<]*)</\1>|] . captures
Just ["h1","shinku"]

後方参照を使って置換する

マッチした文字列全体だけでなく、グルーピングした部分の値を用いて置換したい場合は、次のように MatchPart の matchedString を書き換えます:

{.haskell}
> "<h1>shinku</h1>kawaii" & regex [r|<([^>]*)>([^<]*)</\1>|] %~ (\m -> m & matchedString .~ fromMaybe "" (m ^? captures . ix 1))
"shinkukawaii"

さすがに、これはちょっと分かりにくいかも……。

注意点

なお、regex は IndexedTraversal として定義されていますが、使い方によっては Traversal 則を満たさない場合があります。具体的には regex を用いて文字列を置換し、その結果の文字列が、元々の正規表現にマッチしなくなった場合に Traversal 則を破ります。
とはいえ、これが実用上問題になることがほとんど無いため、あまり気にしなくても大丈夫でしょう2

まとめ

ということで、作って半年くらい放置していた lens-regex の紹介でした。lens ぱわーで正規表現をそこそこ便利に使えるようになりましたが、まだまだ足りない部分もあると思いますので、よければ使ってフィードバックを送っていただければ幸いです。

Happy GHCi 電卓 Life!!

  1. (=~) の型とその値の定義は Text.Regex.Base.Context を参照

  2. lens ライブラリ本体にも filtered のように、使い方によっては Traversal 則を破る物もあります。

15
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?