从一系列解析器组合器构造长度为n的元组

时间:2014-06-22 01:49:36

标签: haskell parser-combinators

  • 问题。有没有办法定义一个运算符,以便由该运算符分隔的一系列值产生一个扁平元组?

我一直在努力找到一个简明扼要的措辞,所以请继续阅读详细信息和示例......

描述

我为Parsec写了几个帮助操作符,从以下开始:

(<@) :: GenParser tok st a -> (a -> b) -> GenParser tok st b
p <@ f = do {
    x <- p ;
    return (f x)
}

(<>) :: GenParser tok st a -> GenParser tok st b -> GenParser tok st (a, b)
p <> q = do {
    x <- p ;
    y <- q ;
    return (x, y)
 }

这些工作如下:

parseTest ((many upper) <@ length) "HELLO"
5

parseTest (many upper) <> (many lower) "ABCdef"
("ABC", "def")

不幸的是,由<>分隔的一系列解析器将导致嵌套元组,例如:

parseTest (subject <> verb <> object) "edmund learns haskell"
(("edmund", "learns"), "haskell")

而不是相对更加强大的:

("edmund", "learns", "haskell")

我正在寻找一种方法来定义<>以便

p1 :: GenParser tok st a ; p2 :: GenParser tok st b ; p3 :: GenParser tok st c
p1 <> p2 :: GenParser tok st (a, b)
p1 <> p2 <> p3:: GenParser tok st (a, b, c)
...

我认为我从未见过一个Haskell程序,其中长度为n的元组类型(在编译时已知)就像这样构造。我怀疑用两种类型定义运算符可能很困难:

GenParser tok st a -> GenParser tok st b) -> GenParser tok st (a, b)
GenParser tok st (a, b) -> GenParser tok st c) -> GenParser tok st (a, b, c)

- 如何在编译时告诉<>产生的元组与任何其他解析器的预期返回类型之间的区别?我只能推测需要额外的语法。

所以,我完全不确定这是一个好主意,甚至可能。即使对我的案子来说这不是一个好主意,我也很想知道怎么做(我很想知道如果不可能!)。

  • 后续问题(如果可以采用这种疯狂的方案)如何注释<> s链中的一个项目被排除在外结果元组?

例如,假设后缀注释<#

p1 :: GenParser tok st a
p2 :: GenParser tok st b
p1 <> keyword "is" <# <> p2 :: GenParser tok st (a, b)

背景

大约2006年,我在大学学习了解析器组合器。我们使用了一个库<@,我相信<>运算符,并且与我的尝试类似地执行。我不知道这个图书馆是什么;它可能是由研究生写的,用于教授我们的课程。在任何情况下,它似乎都不是Parsec,也不是`Text.Parser.Combinators中的基本解析器组合器。

  • 加分问题。 Text.ParserCombinators.ReadP / ReadPrec中的基本解析器组合与Parsec中的基本解析器组合有什么区别?

我似乎记得这个库也是不确定的,每个解析器调用返回可能的解析集和每个解析器的剩余未解析输入。 (成功,完整,明确的解析将导致[(parseresult, "")]。)

  • 最后一个问题。如果这听起来像你听说过的话,你能不能让我知道它是什么(怀旧之情)?

1 个答案:

答案 0 :(得分:6)

函子

我想引起你的注意:

(<@)      :: GenParser tok st a -> (a -> b) -> GenParser tok st b
flip fmap :: (Functor f) => f a -> (a -> b) -> f                b

您是否注意到相似之处?如果我们将f替换为GenParser tok st类型中的flip fmap,则会获得(<@)的类型。此外,这不是理论上的:GenParser tok stFunctor的一个实例。 fmap也可以运营商形式提供,名为<$>。因此,我们可以用两种方式重写您的代码(原始的第一个):

ghci> parseTest ((many upper) <@ length) "HELLO"
5
ghci> parseTest (fmap length (many upper)) "HELLO"
5
ghci> parseTest (length <$> (many upper)) "HELLO"
5

应用型

Functors很不错,但它们不足以包含你的第二个例子。幸运的是,还有另一个类型类:Applicative。现在,Applicative没有形成一个monadic动作的函数,从而产生两个monadic动作中的一对,但它 提供了一些有用的构建块。特别是,它提供了<*>

(<*>) :: f (a -> b) -> f a -> f b

事实证明,我们可以与<$>一起撰写,以重写您的第二个例子:

ghci> parseTest (many upper) <> (many lower) "ABCdef"
("ABC", "def")
ghci> parseTest ((,) <$> many upper <*> many lower) "ABCdef"
("ABC", "def")

如果您不熟悉语法,(,)是创建一对的函数;它的类型为a -> b -> (a, b)

Applicative并不止于此。你正在寻找一种平衡元组的方法;但是,您可以使用Applicative直接创建三元组,而不是创建嵌套元组并将其展平:

ghci> parseTest ((,,) <$> subject <*> verb <*> object) "edmund learns haskell"
("edmund", "learns", "haskell")

恰好,Applicative还有另一个运算符来帮助您处理上一个请求:<**>,它们忽略了第二个或第一个操作数的结果。所以如果你想忽略动词:

ghci> parseTest ((,) <$> subject <* verb <*> object) "edmund learns haskell"
("edmund", "haskell")

奖金问题

如果我没记错的话,ReadP允许在每一步都进行回溯;另一方面,Parsec不允许回溯,除非您使用try组合器或使用该组合子的其他组合器直接或间接地在解析器中对其进行注释。因此,Parsec解析器不会回溯太多,并且可以具有更好的最坏情况性能。


附录:实施<> ...几乎 (不适合胆小的人)

我不知道有什么方法可以实现完全 <>,或者让它适用于所有大小的元组,但如果你'我愿意启用一些Haskell扩展,你可以省去计数,处理任意但固定大小的元组。首先,我们想要一个1元组的类型:

newtype One a = One a deriving (Eq, Read, Show)  -- derive more if you want

现在,我们需要一个具有以下类型的函数:

(<>) :: (Applicative f) => f ()        -> f a -> f (One a)
(<>) :: (Applicative f) => f (One a)   -> f b -> f (a, b)
(<>) :: (Applicative f) => f (a, b)    -> f c -> f (a, b, c)
(<>) :: (Applicative f) => f (a, b, c) -> f d -> f (a, b, c, d)
-- ...

我们如何在Haskell中使用多种类型的函数?当然是Typeclasses!但普通的类型类不会这样做:我们需要功能依赖。另外,我想不出一个合适的名字,所以我只称它为C (如果你能想到一个更好的名字,请在评论中告诉我,我会编辑。)

{-# LANGUAGE FunctionalDependencies #-}
class C a b c | a b -> c, c -> a b where
    (<>) :: (Applicative f) => f a -> f b -> f c

然后,为了实际实现我们的实例,我们需要FlexibleInstances

{-# LANGUAGE FlexibleInstances #-}
instance C ()        a (One a)      where (<>) _ = fmap One
instance C (One a)   b (a, b)       where (<>) = liftA2 $ \(One a)   b -> (a, b)
instance C (a, b)    c (a, b, c)    where (<>) = liftA2 $ \(a, b)    c -> (a, b, c)
instance C (a, b, c) d (a, b, c, d) where (<>) = liftA2 $ \(a, b, c) d -> (a, b, c, d)
-- ...

现在您可以像这样编写解析器:

parseTest (return () <> subject <> verb <> object) "edmund learns haskell"
("edmund", "learns", "haskell")

我们确实必须在它之前写return () <>,这是不可取的。您可以保留先前<>的实现,并将我们的新实现重命名为<+>,然后您可以像这样编写解析器:

parseTest (subject <+> verb <> object) "edmund learns haskell"
("edmund", "learns", "haskell")

<+>用于在前两个之后加入所有解析器。可能有一种方法可以让单个运算符使用OverlappingInstances来完成它,但显而易见的解决方案违反了函数依赖关系。

需要提出的问题

如果您正在考虑使用后一种方法,我建议您不要这样做。首先,如果您要使用元组,那么计算要解析的项目数并不困难。其次,您通常不会首先使用元组,这种方法只有在您尝试获取元组时才有效。但是什么比元组更有意义呢?好吧,一个AST节点。例如,如果您正在为编程语言编写解析器,则可能有类似这样的类型:

data Expression = ...
                | IfExpression Expression Expression Expression
                | ...
                deriving (...)

然后,要解析它,您可以使用Applicative

parseIfExpression = IfExpression
                <$> keyword "if"    *> expression
                <*  keyword "then" <*> expression
                <*  keyword "else" <*> expression

在这里,我们不需要计算物品的数量;它封装在IfExpression类型构造函数的类型中。因为通常你会解析AST,所以元组是无关紧要的,所以这样一个复杂的解决方案似乎是不合理的,特别是因为你必须使用元组的替代方案是如此简单(计算项目的数量并插入适当的元组构造函数)。