Haskell:左偏/短路功能

时间:2013-11-08 21:50:03

标签: parsing haskell

两个班级之前,我们的教授向我们展示了一个Parser模块。

以下是代码:

module Parser (Parser,parser,runParser,satisfy,char,string,many,many1,(+++)) where

import Data.Char
import Control.Monad
import Control.Monad.State

type Parser = StateT String []

runParser :: Parser a -> String -> [(a,String)]
runParser = runStateT

parser :: (String -> [(a,String)]) -> Parser a
parser = StateT

satisfy :: (Char -> Bool) -> Parser Char
satisfy f = parser $ \s -> case s of
    [] -> []
    a:as -> [(a,as) | f a]

char :: Char -> Parser Char
char = satisfy . (==)

alpha,digit :: Parser Char
alpha = satisfy isAlpha
digit = satisfy isDigit

string :: String -> Parser String
string = mapM char

infixr 5 +++
(+++) :: Parser a -> Parser a -> Parser a
(+++) = mplus

many, many1 :: Parser a -> Parser [a]
many p = return [] +++ many1 p
many1 p = liftM2 (:) p (many p)

今天他给我们做了一个任务,介绍了一个名为(< ++)的左偏置或短路版(+++)。他的暗示是我们考虑(+++)的原始实现。当他第一次向我们介绍+++时,这就是他编写的代码,我将其称为原始实现:

infixr 5 +++
(+++) :: Parser a -> Parser a -> Parser a
p +++ q = Parser $ \s -> runParser p s ++ runParser q s

自从我们介绍解析以来,我遇到了很多麻烦,所以它还在继续。

我已经尝试过/正在考虑两种方法。

1)使用“原始”实现,如p +++ q = Parser $ \ s - > runParser p s ++ runParser q s

2)使用最终实现,如(+++)= mplus

以下是我的问题:

1)如果我使用原始实现,模块将无法编译。错误:不在范围内:数据构造函数'Parser'。它使用(+++)= mplus编译好。使用最终实现避免的原始实现有什么问题?

2)如何检查第一个Parser是否返回任何内容?是不是(isNothing(Parser $ \ s - > runParser p s)在正确的轨道上?看起来应该很容易,但我不知道。

3)一旦我弄清楚如何检查第一个Parser是否返回任何内容,如果我将我的代码基于最终实现,那么它会如此简单吗?:

-- if p returns something then
p <++ q = mplus (Parser $ \s -> runParser p s) mzero
-- else
(<++) = mplus

最佳, 杰夫

P.S。 哦,是的,这个代码做了什么?即使它编译,我也不知道如何测试它以确保它按预期工作。

2 个答案:

答案 0 :(得分:4)

这里有很多事情发生了!您正在查看的是(非确定性的)&#34;解析器组合库&#34;您可以在parsecattoparsecuu-parsinglib中找到其他示例...这在Haskell中非常常见,但它确实有点复杂。我会在这里解开核心思想。


要考虑的第一个想法是增量解析的概念&#34;步骤&#34;。这是上面代码中由Parser a表示的内容,您可能会认为它是&#34;运行解析步骤,尝试解析a&#34;类型的内容。

A&#34;解析步骤&#34;包括查看某种输入的字符流,然后需要很多代表一些a类型,然后返回新的a和未使用的剩余字符。在这个描述级别,很容易在Haskell中写出来

String {- input stream -} -> (a {- fresh -}, String {- leftovers -})

这是解析器步骤的基础,值得注意的是,除了解析库之外我们称之为State String a的这种常见习语。

newtype State s a = State { runState :: s -> (a, s) }

>>> :t runState (undefined :: State String a)
String -> (a, String)

我们也可以尝试以这种分解格式构建解析器步骤。考虑一个使用单个字符来创建Int

的解析器
parseInt :: String -> (Int, String)
parseInt (x:xs) = case x of
  '0' -> (0, xs)
  '1' -> (1, xs)
  ...
  '9' -> (9, xs)
  _   -> error "What! Failure!"
parseInt []     = error "What! Another failure!"

>>> parseInt "3leftovers"
(3, "leftovers")

我们可以立即看到这个模型太简单了 - 我们只能通过将错误一直发送到运行时来提供解析器失败。这很危险,并暗示我们对解析器建模不佳。不过,我们可以非常简单地添加失败。

-- String -> Maybe (a, String)

parseInt :: String -> Maybe (Int, String)
parseInt []     = Nothing
parseInt (x:xs) = case x of
  '0' -> Just (0, xs)
  '1' -> Just (1, xs)
  ...
  '9' -> Just (9, xs)
  _   -> Nothing

>>> parseInt "foo"
Nothing

这也是一个非常常见的Haskell主题,即使在名为State Transformer或StateT的解析器之外也是如此。定义看起来像这样

newtype StateT s m a = StateT { runStateT :: s -> m (a, s) }

>>> :t runStateT (undefined :: StateT String Maybe a)
String -> Maybe (a, String)

它允许我们将Maybe体现的失败概念与原始解析器中State的概念结合起来。事实上,这是你的教授用他自己的版本所做的,除了使用Maybe他使用[]

>>> :t runStateT (undefined :: StateT String [] a)
String -> [(a, String)]

允许同时失败(作为空列表[])和多个同时成功。这使得他的解析器不确定 - 它收集并处理每个解析步骤的多个成功。这对于记忆来说可能非常糟糕,但是它是一种使用得很仔细的强大技术。


到目前为止,我所写的内容中还缺少其他内容,但是 - 我们如何将多个解析器组合在一起?运行parseInt三次相当痛苦,例如

parse3Ints :: String -> Maybe ((Int, Int, Int), String)
parse3Ints input = case parseInt input of
  Nothing -> Nothing
  Just (i1, input') -> case parseInt input' of
    Nothing -> Nothing
    Just (i2, input'') -> case parseInt input'' of
      Nothing -> Nothing
      Just (i3, leftovers) -> Just ((i1, i2, i3), leftovers)

唉。我们可以做得更好吗?我们需要以某种方式将失败和input字符串的传递联系在一起。幸运的是,这正是Monad所做的,我们已经看到的所有三种数据类型都已经Monad具有这些确切的行为

instance Monad m => Monad (StateT s m) where ...
instance Monad [] where ...
instance Monad Maybe where ...

请注意,StateT仅在Monad参数为m时才是Monad,这是因为它允许我们将Monad分层,因此需要调用&#34;内部&#34; StateT String Maybe a为了做自己的排序。

结果是,通过将这些简单的函数转换为StateT String [] ado,我们立即使用Monad - 表示法让内置parse3Ints :: StateT String Maybe (Int, Int, Int) parse3Ints = do i1 <- parseInt i2 <- parseInt i3 <- parseInt return (i1, i2, i3) -- or even parse3Ints = liftM3 (,,) parseInt parseInt parseInt 个实例处理我们的问题复杂测序

(+++)

这里最后的兴趣点是教授关于mplus的问题。他在这里使用来自MonadPlus类型类的StateT函数,[]instance MonadPlus [] where mzero = [] mplus as bs = as ++ bs instance MonadPlus m => MonadPlus (StateT s m) where mzero = StateT $ \input -> mzero mplus (StateT sa) (StateT sb) = StateT $ \input -> mplus (sa input) (sb input) 是实例。我们可以看看那段代码

[]

因此,我们可以看到此代码的实际权重位于StateT实例上,因为Monad实例只是将buck传递给其内部mMonadPlus []

[]在做什么?它表示使用&#34;或&#34;组合故障的概念。如果列表Monad中的mzero失败,则mplus as bs会立即失败,as仅在 bs和{{}}失败时才会失败{1}}。我们可以把它写成

mplus mzero a = a
mplus a mzero = a

人们可能会认为这是MonadPlus定义的一种代数法则(尽管这里存在一些争议,但这对代码来说并不重要)。


因此,通过使用mplus实例来组合解析器,会发生什么?简而言之,它允许你&#34;或&#34;解析器一起使它们只有在所有解析器一起失败时才会失败。

(pa +++ pb +++ pc) is mzero ONLY if pa, pb, AND pc are mzero

这在列表Monad中很有效,因为它允许我们一起收集多个成功。没有偏见,因为列表monad会尝试所有不同的解析,它们都只是在列表中没有任何优先级。

我们可以将此与Maybe Monad进行比较,MonadPlus Maybe具有固有的偏见,因为它只会考虑&#34;最好的&#34;在任何给定的时间解析成功。也就是说,我们可以查看instance MonadPlus Maybe where mzero = Nothing mplus Nothing x = x mplus x Nothing = x mplus (Just a) (Just b) = Just a

mplus实例
(+++)

(<++)定义的最后一行,除了最左边的&#34;成功。这是左偏的核心。

但正如我很久以前所说,对我们来说,平等地优先处理所有解析可能是件坏事。在内存中存储非常痛苦的是存储潜在解析的整个树并在每个新角色被消耗时携带它。

为此,我们可以将StateT sa <++ StateT sb = StateT $ \input -> case sa input of [] -> sb input els -> els 偏向sb。这里的想法是我们想要立即返回成功的解析,并且只在&#34;向右&#34;如果我们必须

sa

如果{{1}}解析器没有产生任何结果,我们尝试{{1}}解析器。这意味着我们抛弃了很多潜力&#34;对&#34;在我们的&#34; left&#34;上解析速度。解析。它让我们明智地修剪潜在的解析树。

答案 1 :(得分:2)

  • 1)正如@andras指出的那样,将Parser更改为parser
  • 2 + 3)查看+++
  • 的代码

p +++ q = parser $ \s -> runParser p s ++ runParser q s

我们可以稍微扩展一下,使事情更清楚

p +++ q = parser $ \s -> resP ++ resQ
  where resP = runParser p s
        resQ = runParser q s

这只需要进行少量更改(resP +++ resQ)即可使<++偏向偏见。