我刚刚开始学习Parsec(对Haskell的经验很少),关于使用monad或applicative我有点困惑。在阅读了《Real World Haskell》、《Write You a Haskell》和这里的一个问题之后,总体感觉是更喜欢applicative,但是实际上我不知道到底应该怎么做。
所以我的问题是:
- 哪种方法更受欢迎?
- 可以混合使用monad和applicative吗?(在其中一种比另一种更有用时使用它们)
- 如果上一个答案是肯定的,我应该这样做吗?
我刚刚开始学习Parsec(对Haskell的经验很少),关于使用monad或applicative我有点困惑。在阅读了《Real World Haskell》、《Write You a Haskell》和这里的一个问题之后,总体感觉是更喜欢applicative,但是实际上我不知道到底应该怎么做。
所以我的问题是:
在确定何时使用每个类型之前,值得注意Applicative
和Monad
之间的关键语义差异。比较类型:
(<*>) :: m (s -> t) -> m s -> m t
(>>=) :: m s -> (s -> m t) -> m t
<*>
,您需要选择两个计算,一个是函数,另一个是参数,然后将它们的值通过应用程序进行组合。要部署>>=
,您需要选择一个计算,并解释如何利用其结果值来选择下一个计算。这就是“批处理模式”和“交互式”操作之间的区别。Applicative
(通过添加失败和选择以提供Alternative
)捕获了语法中无上下文的方面。只有当您需要检查来自输入某部分的解析树以决定应使用哪个语法来处理输入的另一部分时,您才需要Monad
提供的额外功能。例如,您可能会读取格式描述符,然后读取该格式的输入。尽量减少使用单子的额外功能可以告诉您哪些值依赖是必不可少的。<*>
组合时,它们都不需要等待对方。在可以使用应用程序的情况下使用应用程序,但在必须使用单子的情况下使用单子,这是速度的公式。ApplicativeDo
的目的是自动化以单子风格编写的代码的依赖关系分析,从而无意中过多地顺序化。ApplicativeDo
是一种制作更多应用程序(在某些情况下意味着更快)的好方法,这些应用程序是以单调风格编写的,而你没有时间进行重构。但除此之外,我认为在可以使用应用程序时使用应用程序,在必须使用单调时使用单调也是更好的方式来了解正在发生的事情。<*>
制作的解析器在表达能力上严格小于您可以使用>>=
制作的解析器 - 尽管额外的表达能力并不总是必要的。这个答案进一步暗示了这些差异:https://dev59.com/R2sz5IYBdhLWcg3wfn66#7863380 - Sam Elliott>>=
而不是do
? - Benjamin Hodgson->
函数空间)。关于现代环境中实现的例子,在Idris中,你可以写[| f x y + z |]
来表示(+) <$> (f <$> x <*> y) <*> z
。 - Cactuslength <$> intersect’ (friendsOf x) (friendsOf y)
http://community.haskell.org/~simonmar/papers/haxl-icfp14.pdf。除了Haxl之外,还有其他具有并行性实现的Applicative实例示例吗? - Rob Stewartrepeat
是return
,而join
则取一个无限方阵的对角线。当然,如果你只想要对角线,计算甚至thunks的矩阵是愚蠢的:>>=
至少生成一个三角形。在这种意义上合并流的明智方法是通过压缩,这正是适用行为。它可能不完全是并行性,但它是另一个例子,其中<*>
的默认实现从>>=
中效率低下地过度顺序化。 - pigworker总的来说,从你觉得最合理的地方开始。然后考虑以下内容。
使用Applicative
(甚至是Functor
)是很好的实践。通常情况下,像GHC这样的编译器更容易优化这些实例,因为它们可以比Monad
更简单。我认为社区的一般建议是在AMP之后尽可能地使约束更加通用。我建议使用GHC扩展ApplicativeDo
,因为您可以统一使用do
符号,只有在需要时才会得到Applicative
约束。
由于ParsecT
解析器类型既是Applicative
的实例,也是Monad
的实例,所以您可以混用两者。在某些情况下,这样做更易读——这完全取决于情况。
另外,考虑使用megaparsec
。 megaparsec
是一个更活跃的维护者,通常更为干净、更新的parsec
分支。
重新阅读我的回答和评论后,有两个事情我没有澄清清楚:
使用Applicative
的主要好处是,对于许多类型,它允许更有效率的实现(例如,(<*>)
比ap
性能更高)。
如果您只想编写类似(+) <$> parseNumber <*> parseNumber
的内容,则无需使用ApplicativeDo
- 它会变得更冗长。只有在您开始编写非常长或嵌套的applicative表达式时,才应使用ApplicativeDo
。
<$
,fmap
,*>
,<*
和/或<*>
;而其他类型则不支持。Data.Sequence
每向下迈出一步都会获得主要的速度提升(尽管<*
目前缺失;我将不得不修复它)。另一方面,列表只会获得微小的提升(除了map/coerce的特殊情况)。 - dfeuerMonad
更简单”的意思。但如果你在评论,那么我的回答就不明显了。 :) 我不想在这里过分强调这一点,因为当我查看ParsecT
的(<*>)
时,它似乎与(>>=)
相比并没有额外的效率。 - Alec继@pigworker之后(我在这里太新了,无法评论),值得注意的是join $ fM <*> ... <*> ... <*> ...
也是一种模式。它可以像<$>
和<*>
一样,为您提供“bind1,bind2,bind3…”家族。
作为一种风格上的东西,当你足够熟悉组合器时,你可以使用do
与使用let
相同:作为一种突出显示想要命名某些内容的方式。例如,在解析器中,我倾向于更经常地命名事物,因为那可能对应于规范中的名称!