将子例程放入解析器的优雅方法是什么?

时间:2013-08-02 01:40:17

标签: parsing haskell subroutine coroutine

问题

我正在为自我教育推出一个非确定性解析器,它看起来像:

newtype Parser a b = P { runP :: [a] -> [(b, [a])] }

我希望能够放入一个可能形式的子例程:[a] -> [b],它接受​​一个字符缓冲区并将其发送到结果列表。这里的技巧是子程序本身是一个有状态计算,并在每次成功调用时转换为状态(将其视为有限状态机)。具体做法是:

  1. 如果子例程输出空列表[],则解析器会将另外一个字符串插入缓冲区并将其输出到子例程,该子例程再次运行。
  2. 如果子例程输出非空列表[b],则首先刷新缓冲区,然后解析器将另外一个字符串插入缓冲区,使其生成子例程。 [b]存储在某个地方
  3. 在达到逃生条件之前,步骤1和2一遍又一遍地运行。所有中间结果应以某种方式组合。
  4. 达到转义条件后,子程序将结果bs返回给解析器,并将其与剩余的as流组合,如下所示:

    rs = fmap(flip(,)as)bs :: [(b,[a])]

  5. 因此满足runP

    的签名

    功能可能具有此签名:withf :: ([a] -> [b]) -> Parser a b

    重要的是withf g必须是解析器,因此我可以使用<*>构建更大的解析器。请注意函数签名建议g是一个纯函数,所以它不太可能是正确的。

    尝试过的解决方案

    我尝试使用各种协同程序包实现这一点,但对我来说,将lift解析器转换为协同计算上下文更有意义,将其与一个也转换为上下文的转换器组合,这意味着它&# 39; s不再是解析器。

    我还尝试将withf实现为可以访问Parser值构造函数的原始函数。基本上将步骤1..4转换为代码。我在这里遇到的最大问题是谁负责哪些信息:

    1. 缓冲区状态
    2. 中间结果的状态
    3. 中间结果如何组合的逻辑。
    4. 如何实施逃避条件,或者更好地将其组合到withf
    5. 我还尝试了各种自制的coroutine实现,直接进入解析器(所以不使用上面定义的Parser类型),但收效甚微。

      任何能指出我正确方向的人都非常感谢。

2 个答案:

答案 0 :(得分:3)

首先,让我们在解析器中使用MonadPlus而不是[]。它会使它更通用并稍微澄清一下代码(我们不会有这么多嵌套的[]):

newtype Parser a m b = P { runP :: [a] -> m (b, [a]) }

我建议你改变子程序的签名。你需要的是:

  • 表示子程序是否需要更多输入,
  • 将状态保持在某个地方。

这可以通过此类型签名轻松完成:

newtype Sub a b = Sub { runSub :: Either (a -> Sub a b) [b] }

子程序产生结果,或者请求新输入并产生新的子程序。这样,您可以通过将其传递到返回的子例程来保持您需要的任何状态。转换功能将如下所示:

withf :: (MonadPlus m) => Sub a b -> Parser a m b
withf start = P $ f (runSub start)
  where
    f (Right bs) xs   = msum [ return (b, xs) | b <- bs ]
    f (Left r)   []   = mzero    -- No more input, can't proceed.
    f (Left r) (x:xs) = f (runSub (r x)) xs

更新:我们可以采取的另一种方法是认识到解析器实际上是StateT转换器,其状态为[a]

type Parser a m b = StateT [a] m b

runP :: (Monad m) => Parser a m b -> [a] -> m (b, [a])
runP = runStateT

确实,runP正是runStateT

这样,我们免费为Monad获取Parser个实例。现在我们可以将任务分成更小的块。首先,我们创建一个消耗一个输入的解析器,或者失败:

receive :: (MonadPlus m) => Parser a m a
receive = get >>= f
  where
    f []        = mzero    -- No more input, can't proceed.
    f (x:xs)    = put xs >> return x

然后用它来描述withf

withf :: (MonadPlus m) => Sub a b -> Parser a m b
withf start = f (runSub start)
  where
    f (Right bs) = msum (map return bs)
    f (Left r)   = receive >>= f . runSub . r

请注意,如果mMonadPlus,那么StateT s m也是MonadPlus,因此我们可以直接使用mzeromsum Parser

答案 1 :(得分:2)

首先,让我们定义一个新的数据类型来表示解析的可能结果。

data Step r = Done | Fail | Succ r

解析器可以使用Done结束,使用Fail指示失败的解析,也可以使用r成功返回已解析的值Succ r

我们会将Step数据类型设为Monoid类型类的实例

instance Monoid (Step r) where
    mempty = Done
    Done   `mappend` _ = Done
    Fail   `mappend` x = x
    Succ r `mappend` _ = Succ r

如果我们的解析器是Done,我们应该立即终止。 Fail表示我们应该检查下一个Step的结果,以获得可能的成功。当然,Succ r意味着我们已经成功解析了一个值。

现在让我们为Parser定义一个类型同义词。它需要能够累积解析结果(Writer)并维护一个纯状态,表示尚未消耗的输入(State)。

{-# LANGUAGE FlexibleContexts #-}                                                                   

import Control.Monad.State
import Control.Monad.Writer
import Data.List
import Data.Foldable

type Parser w s = WriterT w (State s)

evalParser :: Parser w s r -> s -> w
evalParser = evalState . execWriterT

这是实际的解析器

parser :: (MonadState [s] m, MonadWriter [w] m) => ([s] -> Step [w]) -> m ()
parser sub = do
    bufs <- gets inits
    -- try our subroutine on increasingly long prefixes until we are done,
    -- or there is nothing left to parse, or we successfully parse something
    case foldMap sub bufs of
         Done   -> return ()
         Fail   -> return ()
         Succ r -> do
            -- record our parsed result
            tell r
            -- remove the parsed result from the state
            modify (drop $ length r) 
            -- parse some more
            parser sub

和一个简单的测试用例

test :: String
test = evalParser (parse sub) "aabbcdde"
    where sub "aabb" = Succ "aabb"
          sub "cdd"  = Succ "cdd"
          sub "e"    = Done
          sub _      = Fail

-- test == "aabbcdd"
相关问题