【问题标题】:Using Haskell Parsec to parse a regular expression in one pass使用 Haskell Parsec 一次性解析正则表达式
【发布时间】:2013-05-11 09:34:39
【问题描述】:

初步解释

我正在尝试使用自定义正则表达式引擎进行一些测试,但我厌倦了手动编写 NFA,因此我尝试制作一个解析器,但收效甚微。通常当人们解析一个正则表达式时,他们会创建多个中间结构,这些结构最终会被转换成最终的机器。对于我简单的 NFA 定义,我相信解析实际上可以一次性完成,尽管我还没有确定 (a) 为什么它实际上不能或 (b) 怎么做,尽管我的解析器可以解析非常简单声明。

(简化的)状态实体的定义如下 [1]:

type Tag = Int

data State a =
    Literal Tag a (State a)
  | Split (State a) (State a)
  | OpenGroup Tag (State a)
  | CloseGroup Tag (State a)
  | Accept                     -- end of expression like "abc"
  | Final                      -- end of expression like "abc$"

标签允许 Show 和 Eq 实例,即使最终 NFA 可以包含循环。例如,为表达式建模

-- "^a+(b*)c$"

我可以使用 [2]

c = Literal 3 'c' $ Final 1
b = OpenGroup 1 $ Literal 2 'b' bs
bs = Split b $ CloseGroup 1 c
expr = Literal 1 'a' $ Split expr bs

我通过将 Thompson NFA 实现的 C 实现移植到 Haskell 为这个语法(没有组标签)制作了一个基于堆栈机器的解析器,但这需要两次通过 [3] 来构建,并且需要第三次来结束在上面描述的结构中。

所以为了通过 Parsec 构建这个结构,我阅读了这个网站上关于递归构建类似 List 的结构的条目,并提出了以下内容:

import           Control.Applicative
import           Text.Parsec         hiding (many, optional, (<|>))
import           Text.ExpressionEngine.Types
import qualified Text.ExpressionEngine.Types as T

type ParserState = Int
type ExpParser = Parsec String ParserState
type ExpParserS a = ExpParser (T.State a)

parseExpression :: String -> T.State Char
parseExpression e = case runParser p 1 e e of
  Left err -> error $ show err
  Right r -> r
where
  p = p_rec_many p_char $ p_end 1

p_rec_many :: ExpParser (T.State a -> T.State a) -> ExpParserS a -> ExpParserS a
p_rec_many p e = many'
  where
    many' = p_some <|> e
    p_some = p <*> many'

p_end :: Int -> ExpParserS a
p_end n = (Final n <$ char '$') <|> (Accept n <$ eof)

step_index :: ExpParser Int
step_index = do
  index <- getState
  updateState succ
  return index

p_char = do
  c <- noneOf "^.[$()|*+?{\\"
  i <- step_index
  return $ Literal i c

这足以解析像“ab”和“abc$”这样的字符串[4]。

问题

当我进入下一步时,问题就来了:解析“|” for 或语句。这应该工作的方式是一个字符串,如:

-- "(a|b|c)$"

应该创建以下结构:

final = Final 1
c = Literal 3 'c' final
b = Split (Literal 2 'b' final) c
a = Split (Literal 1 'a' final) b

因此,这意味着将构建或语句的解析器必须采用其后出现的替代表达式并将其传递给所有分支(我不相信将 Split 更改为采用列表会改变任何东西,因为每个条目仍然必须收到相同的以下表达式)。我的尝试是:

p_regex :: T.State Char -> ExpParser (T.State Char)
p_regex e = do
  first <- p_rec_many p_char $ pure e
  (Split first <$> (char '|' *> p_regex e)) <|> return first

主解析器变为:

parseExpression :: String -> T.State Char
parseExpression e = case runParser p 1 e e of
  Left err -> error $ show err
  Right r -> r
  where
    p = p_regex <*> p_end 1

但这无法进行类型检查 [5]。我希望这是正确的,因为 p_regex 必须有一个构建的(状态 a)对象提供给它,并且使用 p_rec_many 构建“文字”链似乎也可以这种方式工作。

也许我应该使用 buildExpressionTable?这可能有助于解决这个特定问题,因为我可以将 ('$' eof) 设为最高优先级。我开始尝试它,但我无法想象我将如何处理诸如星号加号和问号运算符之类的事情,因为它们都必须引用自己。

(编辑:我再次尝试使用 buildExpressionTable,但在我看来,这对于我想要做的事情来说过于简单了。它本身无法处理堆叠的后缀运算符 [例如“a?*”],而我的将“'$' eof”设为最高优先级的计划也不起作用,因为它只会附加到最后一个解析的“术语”,而不是整个字符串。即使我可以做到,“$”运算符将被向后应用:它应该是最后一个解析的术语并馈送到前一个术语。我使用这个越多,我就越想知道我是否不应该在解析之前反转表达式字符串。)

问题

那么,我做错了什么?我确信有一种方法可以做我想做的事情,但到目前为止我还没有弄清楚。感谢您的宝贵时间。

脚注

[1] 如果您想确切了解我实际使用的内容,可以找到here

[2] Open/CloseGroup 标签的想法是在 NFA 运行时跟踪组匹配。列出的表达式中的位置可能不完全正确,但是如果遇到 CloseGroup 标签,这种方式将正常工作,仅在找到相应的 OpenGroup 时才创建捕获组(即在上面的示例中,我们只会在至少一个时创建捕获看到了“b”)。

所有其他标签构造都是正确的,我已经测试过这个 NFA 与预期的字符串匹配。

[3] Thompson 实现在here 中进行了描述,我的端口可以在here 中看到。这完美地构建了 NFA 子集,但在生成的结构中,每个下一个状态都将被包裹在 Just 中。这是因为我使用 Nothing 来表示悬空指针,后面的步骤将在正确的下一步中修补。我可以通过将所有(Just state)条目转换为(state)条目来将此结构转换为上面列出的结构,但这将是第三遍。此实现已经需要第一次将正则表达式转换为后缀表示法。

[4] 导致

Literal 1 'a' (Literal 2 'b' (Accept 1))

Literal 1 'a' (Literal 2 'b' (Literal 3 'c' (Final 1)))

分别。

[5]

Couldn't match expected type `a0 -> b0'
        with actual type `ExpParser (T.State Char)'
Expected type: T.State Char -> a0 -> b0
Actual type: T.State Char -> ExpParser (T.State Char)
In the first argument of `(<*>)', namely `p_regex'
In the expression: p_regex <*> p_end 1

【问题讨论】:

    标签: regex haskell parsec


    【解决方案1】:

    您可能不会得到很多答案,因为这是一个巨大的问题,需要阅读大量论文才能让任何人考虑编写答案。

    话虽如此,但很巧合,我这周碰巧自己尝试从正则表达式构建 NFA。 ;-)


    好的,所以立即问题是

    Couldn't match expected type `x -> y` with actual type `Parser`.
    

    在英语中,这意味着在某个地方你有一个函数而不是解析器。快速浏览一下您的代码表明您已经编写了

    where
      p = p_regex <*> p_end 1
    

    但是p_regex 接受 1 个参数,而您没有提供一个。 就是你的代码不进行类型检查的原因。


    好的,退后一步,您的实际问题是什么?您想将正则表达式解析为 NFA,但论文希望您将正则表达式转换为后缀表示法,然后解析它,然后构建 NFA?

    看起来应该是可能的。当我实现这个时,我将解析和 NFA 生成作为单独的步骤,纯粹是为了检查解析器是否工作以及 NFA 生成是否单独工作。但这听起来应该是可能的。 Parsec 允许您拥有用户状态,因此您可以将其用作堆栈来存储 NFA 片段。 (或者如果您愿意,也可以明确地传递它。)

    如果您想要更准确的答案,您可能需要将其缩减为一个更小、更集中的问题。

    【讨论】:

    • 感谢您的回答。我知道我的问题很长,但很难:如果你把问题写得太长,没有人会读它,但如果你把它写得太短,那么你会邀请驱动器通过题外话回答“而不是做你真正想做的事, 你应该用 X 代替”,这是浪费大家的时间,而且会妨碍人们给出真正的答案。在这种情况下,我正在考虑将答案归功于您,因为您确实回答了我的一个问题:“有可能吗?”接受我自己的答案可能很蹩脚。
    • 哦,P.S.:我有p_regex &lt;*&gt; p_end 1 的原因是因为我希望它表现得像一个应用程序,即:(+) &lt;$&gt; [1] &lt;*&gt; [2] 会。我预料到了这一点,因为我在其他地方用 parsec 使用了这种应用风格,而且效果很好。我认为在这种情况下,问题在于我正在创建一个依赖于解析的函数,该解析直到最后才会发生。
    【解决方案2】:

    好的,所以问题基本上是:给定一个递归数据结构(在问题中定义),我如何创建一个解析器来一次性构建我的表达式。我最初的尝试本质上是一种“应用程序”。只要没有条件分支,我就能够建立递归结构。但是对于正则表达式解析,需要分支,这就是为什么我的方法不适用于or 语句。

    所以要解决这个问题,我需要有一些状态。在函数式语言中携带状态的一种好方法是使用部分应用的函数。我已经有了一个基础,上面p_char 的签名是:

    p_char :: ExpParser (T.State Char -> T.State Char)
    

    所以我需要将它们组合在一起的是组合多个(T.State Char -&gt; T.State Char) 函数的组合器。因此,有了这种洞察力,排序就变成了:

    p_many1 :: ExpParser (T.State Char -> T.State Char) -> ExpParser (T.State Char -> T.State Char)
    p_many1 p = do
        f <- p
        (p_many1 p >>= return . (f .)) <|> return f
    

    现在对于or 语句,我们需要的是接受像“a|b|c”这样的表达式并创建如下函数:

    \e -> Split (Literal 1 'a' e) (Split (Literal 2 'b' e) (Literal 3 'c' e))
    

    为此,我们可以使用这个:

    p_splitBy1 :: ExpParser (T.State Char -> T.State Char) -> Parsec String ParserState Char -> ExpParser (T.State Char -> T.State Char)
    p_splitBy1 p sep = do
        f <- p
        (sep >> p_splitBy1 p sep >>= return . (\f' e -> Split (f e) (f' e))) <|> return f
    

    这确实创建了我需要的结构。所以如果以后有其他人遇到类似的问题,也许这个问题/答案可以派上用场。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-03-06
      • 1970-01-01
      • 2015-07-08
      • 2012-08-23
      • 2014-06-08
      相关资源
      最近更新 更多