当您使用 Haskell 并且正则表达式失败时,请使用 Parsec!我个人认为这是我在使用其他语言时想念的 Haskell 的最佳特性之一,解析上下文无关语言甚至上下文敏感语言与使用正则表达式一样容易,甚至更容易。
import Text.ParserCombinators.Parsec
import Control.Applicative ((<$>),(<*>),(*>),(<*))
import Control.Monad
import Text.Printf
-- Handy helper to concat successive parsers that return strings
infixl 4 <++>
f <++> g = (++) <$> f <*> g
-- Parse a balanced number of brackets
brackets :: Parser String
brackets = string "(" <++> (join <$> many brackets) <++> string ")" <|> many1 (noneOf "()")
-- Parser that will perform your example replace
replaceShape :: Int -> String -> Parser String
replaceShape line name = printf "TrackedShape (%i \"%s\" %s)" line name <$> (string "Shape " *> brackets)
在 GHCi 中测试:
> parseTest (replaceShape 10 "John") "Shape (Rectangle 3 5)"
"TrackedShape (10 \"John\" (Rectangle 3 5))"
如果您以前没有使用过 Applicatives 甚至 Applicative 解析器,上面的代码可能看起来相对难以理解。然而,就像正则表达式一样,一旦你习惯了它,它就是一种很好的工作方式。那里有很多非常完整的解析器教程,Learn you a Haskell 对 Applicatives 有一些很好的解释。<$>
是 fmap 的中缀版本,并<*>
以与>>=
. 所以(++) <$> f <*> g
我们案例中的表达式等价于
do
a <- f
b <- g
return $ a ++ b
事实上,函数可以这样写(有一个 Applicative 和 Monad 接口),但是 applicative 风格允许非常简洁的,像正则表达式的解析器。
示例中使用的其他函数是<|>
提供交替(即以这种方式或那种方式解析。因此在括号示例中,两个备选方案是打开一个新括号或解析中间不是括号的内容。)<*
和*>
,它们是一样的,<*>
只是它们丢弃了一侧的结果。< 指向保留的结果。
请注意,我已经注意到实现了检测行号的额外功能,但是 Parsec 可以做到这一点,因为解析器 monad 实际上是一个 monad 转换器,因此可以放在状态 monad 之上以在解析期间保留任意信息。Parsec 有一个非常丰富的功能集来做这样的事情,所以它绝对是你想要完成这项工作的工具。