秒差距:应用程序与单子

Parsec: Applicatives vs Monads

我刚开始使用 Parsec(在 Haskell 方面经验很少),我对使用 monad 或应用程序有点困惑。读完"Real World Haskell","Write You a Haskell"的总体感觉还有一个问题是applicatives是首选,但我真的不知道。

所以我的问题是:

一般来说,从对您来说最有意义的内容开始。之后再考虑以下。

最好尽可能使用 Applicative(甚至 Functor)。通常像 GHC 这样的编译器更容易优化这些实例,因为它们比 Monad 更简单。我认为一般社区建议 post-AMP has been to make as general as possible your constraints. I would recommend the use of the GHC extension ApplicativeDo 因为您可以统一使用 do 表示法,而只在需要时才获得 Applicative 约束。

由于 ParsecT 解析器类型是 ApplicativeMonad 的实例,您可以将两者混合搭配。在某些情况下这样做更具可读性——这完全取决于具体情况。

此外,考虑使用 megaparsecmegaparsecparsec.

的一个更积极维护的更近期的分支

编辑

有两件事,重新阅读我的回答和评论,我确实没有做好澄清:

  • 使用 Applicative 的主要好处是,对于许多类型,它允许更有效的实现(例如,(<*>)ap 更高效)。

  • 如果您只想写类似 (+) <$> parseNumber <*> parseNumber 的内容,则无需放入 ApplicativeDo - 那样会更冗长。仅当您开始发现自己正在编写非常长或嵌套的应用表达式时,我才会使用 ApplicativeDo

可能值得关注 ApplicativeMonad 之间的关键语义差异,以确定它们何时适用。比较类型:

(<*>) :: m (s -> t) -> m s -> m t
(>>=) :: m s -> (s -> m t) -> m t

要部署 <*>,您选择两个计算,一个是函数,另一个是参数,然后它们的值由应用程序合并。要部署 >>=,您选择一个计算,然后解释您将如何利用其结果值来选择下一个计算。就是"batch mode"和"interactive"运算的区别。

在解析方面,Applicative(扩展失败并选择给出 Alternative)捕获语法的 上下文无关 方面。仅当您需要从输入的一部分检查解析树以决定输入的另一部分应该使用什么语法时,您才需要 Monad 提供的额外功能。例如,您可能会读取格式描述符,然后是该格式的输入。最大限度地减少对 monad 额外功能的使用会告诉您哪些值依赖性是必不可少的。

从解析转向并行,这种仅将 >>= 用于基本值依赖的想法可以让您清楚地了解分散负载的机会。当两个计算与 <*> 结合时,不需要等待另一个。 Applicative-when-you-can-but-monadic-when-you-must 是速度的公式。 ApplicativeDo 的要点是自动对以 monadic 风格编写的代码进行依赖性分析,从而意外地过度序列化。

您的问题还与编码风格有关,对此有不同意见。但是让我告诉你一个故事。我从 Standard ML 来到 Haskell,在那里我习惯于以直接的方式编写程序,即使他们做了一些顽皮的事情,比如抛出异常或改变引用。我在 ML 中做什么?致力于实现超纯类型理论(出于法律原因,可能无法命名)。在那种类型理论中工作时,我无法编写使用异常的直接式程序,但我编写了应用组合器作为一种尽可能接近直接式的方法。

当我搬到 Haskell 时,我惊恐地发现人们似乎认为使用伪命令式 do-notation 编程只是对最轻微的语义不纯的惩罚(当然,除了,来自非终止)。早在我掌握语义区别之前,我就采用了应用组合器作为一种风格选择(并且更接近直接风格 "idiom brackets"),即它们代表了 monad 接口的有用弱化。我只是不喜欢(现在仍然不喜欢)do-notation 需要表达结构的碎片化和事物的无端命名。

也就是说,使函数式代码比命令式代码更紧凑和可读的相同因素也使应用程序风格比 do-notation 更紧凑和可读。我很欣赏 ApplicativeDo 是一种很好的方法,可以使您没有时间以 monadic 风格编写的程序更具应用性(在某些情况下,这意味着 更快 )重构。但除此之外,我认为 applicative-when-you-can-but-monadic-when-you-must 也是查看正在发生的事情的更好方法。

继@pigworker 之后(唉,我太新了,无法发表评论)值得注意的是 join $ fM <*> ... <*> ... <*> ... 作为一种模式。它让你拥有一个 "bind1, bind2, bind3..." 家庭,就像 <$><*> 给你一个 "fmap1,fmap2,fmap3" 家庭一样。

作为一种文体,当你对组合器使用得足够多时,可以像使用 let: 一样使用 do 作为一种突出显示你想要的方式命名某事。例如,我倾向于在解析器中更频繁地命名事物,因为这可能对应于规范中的名称!