【发布时间】:2011-10-22 19:01:00
【问题描述】:
似乎有一个共识,您应该将 Parsec 用作应用程序而不是 monad。应用解析比单子解析有什么好处?
- 风格
- 性能
- 抽象
monadic 解析出来了吗?
【问题讨论】:
标签: haskell monads parsec applicative
似乎有一个共识,您应该将 Parsec 用作应用程序而不是 monad。应用解析比单子解析有什么好处?
monadic 解析出来了吗?
【问题讨论】:
标签: haskell monads parsec applicative
一元解析和应用解析之间的主要区别在于如何处理顺序组合。对于应用解析器,我们使用 (<*>),而对于 monad,我们使用 (>>=)。
(<*>) :: Parser (a -> b) -> Parser a -> Parser b
(>>=) :: Parser a -> (a -> Parser b) -> Parser b
一元方法更灵活,因为它允许第二部分的语法依赖于第一部分的结果,但我们在实践中很少需要这种额外的灵活性。
您可能认为拥有一些额外的灵活性并没有什么坏处,但实际上它可以。它阻止我们在不运行解析器的情况下对它进行有用的静态分析。例如,假设我们想知道解析器是否可以匹配空字符串,以及匹配中可能的第一个字符。我们想要函数
empty :: Parser a -> Bool
first :: Parser a -> Set Char
使用应用解析器,我们可以轻松回答这个问题。 (我在这里有点作弊。假设我们有一个数据构造函数对应于我们的候选解析器“语言”中的(<*>) 和(>>=))。
empty (f <*> x) = empty f && empty x
first (f <*> x) | empty f = first f `union` first x
| otherwise = first f
但是,对于单子解析器,我们不知道第二部分的语法是什么,而无需知道输入。
empty (x >>= f) = empty x && empty (f ???)
first (x >>= f) | empty x = first x `union` first (f ???)
| otherwise = first x
通过允许更多,我们能够减少推理。这类似于动态和静态类型系统之间的选择。
但这有什么意义呢?我们可以将这些额外的静态知识用于什么?好吧,我们可以使用它来避免在 LL(1) 解析中回溯,方法是将下一个字符与每个备选的 first 集合进行比较。我们还可以通过检查两个备选方案的first 集是否重叠来静态确定这是否会产生歧义。
另一个例子是它可以用于错误恢复,如 S. Doaitse Swierstra 和 Luc Duponcheel 的论文 Deterministic, Error-Correcting Combinator Parsers 中所示。
但是,通常情况下,您正在使用的解析库的作者已经在应用解析和单子解析之间做出了选择。当像 Parsec 这样的库同时公开这两个接口时,选择使用哪一个纯粹是一种风格。在某些情况下,应用程序代码比一元代码更容易阅读,有时恰恰相反。
【讨论】:
empty 测试也可以应用于单子解析器时。原因是我们可以通过对空字符串应用解析器x 来获取您命名为??? 的值。更一般地说,您可以将空字符串输入解析器,看看会发生什么。类似地,至少可以以函数形式first :: Parser a -> (Char -> Bool)获得首字符集。当然,将后者转换为Set Char 会涉及到字符的低效枚举,这就是应用函子的优势所在。
empty。或者你可以,但这就像用“让我们运行程序看看它是否停止”来回答 [en.wikipedia.org/wiki/Halting_problem]。
let x = pure f <*> y <*> x in empty x 会怎样。如果empty y 是False,那么计算不会终止……只是要指出,它并不是那么简单。
如果解析器是纯粹的应用程序,则可以在运行之前分析其结构并“优化”它。如果解析器是单子的,它基本上是一个图灵完备的程序,并且对其执行几乎任何有趣的分析都相当于解决停机问题(即,不可能)。
哦,是的,还有风格上的差异......
【讨论】:
(>>=) 的历史错误,使得实例无法提供像 ap 这样的运算符的更优化实现。 Applicative 类避免了这个错误,并暴露了<*>(相当于ap)。
Applicative 实例,而应用程序解析器通常不支持 >>=。
>>= 接受左侧解析器的结果并将其传递给右侧的解析器,从而使生成的解析器无法静态分析,因为现在解析器本身已经完成。 <*> 和 <$> 不检查参数,因此虽然解析器本身的输出是图灵完整的,因为您可以将任何内容放在 <$> 的左侧,解析方面本身受到限制并且可以静态分析。跨度>
我可以看出,我更喜欢应用程序解析器而不是单子解析器的主要原因与在任何情况下更喜欢应用程序代码而不是单子代码的主要原因相同:应用程序功能不那么强大,使用起来更简单。
这是更通用的工程格言的一个实例:使用完成工作的最简单的工具。不要使用叉车。不要使用台锯切割优惠券。不要在IO 中编写代码,因为它可能是纯粹的。把事情简单化。
但有时,您需要Monad 的额外功能。一个明确的迹象是,当您需要根据目前已计算的内容更改计算过程时。在解析方面,这意味着根据到目前为止已经解析的内容来确定如何解析接下来的内容;换句话说,您可以通过这种方式构建上下文相关的语法。
【讨论】:
借助 Parsec,使用 Applicative 的好处就是风格。 Monad 的优势在于它更强大——你可以实现上下文相关的解析器。
Doaitse Swierstra 的 UU 解析如果仅用于应用程序,效率会更高。
【讨论】:
Strings,语言是所有字母都相等的单词集合。作为一个单子解析器,这非常容易:option [] (anyToken >>= many . exactToken)(这里的anyToken 和exactToken 实际上并不是 Parsec 库的一部分,但可能应该是;问我是否不确定他们做了什么)。相应的应用解析器看起来如何?
Monad 严格来说是一种比 Applicatives 更具功能性的抽象。你可以写
instance (Monad m) => Applicative m where
pure = return
(<*>) = ap
但是没有办法写
instance (Applicative a) => Monad a where
return = pure
(>>=) = ???
所以是的,这本质上是风格的问题。我想如果你使用 因为Applicative 是一个比Monad 更小的接口。 ,这意味着return 和ap,那么使用pure 和<*> 应该不会有性能损失。<*> 有时可以比ap 得到更高的优化。 (但通过巧妙的 GHC 重写规则,通常可以实现相同的优化。)
monadic 解析出来了吗?
由于 Monad 是 Applicatives 的子集,我会得出结论,applicative 解析是 monadic 解析的子集。
【讨论】:
Monad 实例的类型是有Applicative 实例的类型的子集。当谈到“Monads”和“Applicatives”时,通常指的是类型,而不是操作。
return和ap,那么使用pure和<*>应该不会有性能损失。” IIRC 并非如此。有很多情况(解析器只是其中之一)<*> 的性能优于 ap。