あどけない話

インターネットに関する技術的な話など

Haskellで正規表現リテラル

Haskell正規表現を使いたくなったとします。Haskell には正規表現リテラルがないので、文字列リテラルで代用します。Haskell正規表現では、メタ文字の多くがバックスラッシュを使わずに定義されています。そこで、文字列中にバックスラッシュを書くことはあまりありませんが、それでも書く必要に迫られることがあります。

たとえば、Window のファイル名の区切り文字であるバックスラッシュや、丸括弧にマッチさせたい場合は、たくさんのバックラッシュを書く必要があります。

> :m Text.Regex.Posix
> "\\foo\\bar.c" =~ "\\\\foo\\\\bar\\.c" :: Bool
→ True
> "(1+2)/(3+4)" =~ "\\([^)]+\\)" :: (Int,Int)
→ (0,5)

トムさんから、QuasiQuote を使えばバックスラッシュをなくせるのではないかというアイディアを頂いたので、実装してみました。

{-# LANGUAGE FlexibleContexts #-}
module RegexLiteral where

import Data.String
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Text.Regex.Posix

re :: QuasiQuoter
re = QuasiQuoter parseExprExp parseExprPat

parseExprExp :: String -> ExpQ
parseExprExp x = return . LitE . StringL $ x

parseExprPat :: String -> PatQ
parseExprPat = undefined

instance IsString Regex where
  fromString s = make s
    where
      make :: String -> Regex
      make = makeRegex

正規表現リテラルは、ちょっと長いのですが、 "[$re|...|]" と表現します。

% ghci -XQuasiQuotes RegexLiteral.hs
> "\\foo\\bar.c" =~ [$re|\\foo\\bar\.c|] :: Bool
→ True
> "(1+2)/(3+4)" =~ [$re|\([^)]+\)|] :: (Int,Int)
→ (0,5)

ちなみに IsString のインスタンスにしているので、Regex を必要とするところにも、そのまま使えます。

% ghci -XQuasiQuotes -XOverloadedStrings RegexLiteral.hs
> ([$re|\\(foo)\\(bar\.c)|] :: Regex) `matchOnceText` ("\\foo\\bar.c" :: String)
→ Just ("",array (0,2) [(0,("\\foo\\bar.c",(0,10))),(1,("foo",(1,3))),(2,("bar.c",(5,5)))],"")
> ([$re|\([^)]+\)|] :: Regex) `matchAllText` ("(1+2)/(3+4)" :: String)
→ [array (0,0) [(0,("(1+2)",(0,5)))],array (0,0) [(0,("(3+4)",(6,5)))]]

対話的なこの例では、型を決めるために型の指定が面倒になっていますが、ソースに書けばこのような煩わしさはありません。

という訳で、fromString as eval プロジェクトはいけそうです! (謎)