由于许多应用程序也是单子,我觉得这个问题确实有两个方面。
当两者都可用时,为什么我要使用应用程序接口而不是单子接口?
这主要是风格问题。尽管 monad 具有do
-notation 的语法糖,但使用 applicative 样式通常会导致更紧凑的代码。
在这个例子中,我们有一个类型Foo
,我们想要构造这个类型的随机值。使用 monad 实例IO
,我们可以写
data Foo = Foo Int Double
randomFoo = do
x <- randomIO
y <- randomIO
return $ Foo x y
applicative 变体要短得多。
randomFoo = Foo <$> randomIO <*> randomIO
当然,我们可以使用liftM2
来获得类似的简洁性,但是应用风格比必须依赖于特定于arity的提升函数更简洁。
在实践中,我发现自己使用应用程序的方式与我使用无点样式的方式非常相似:当一个操作被更清楚地表示为其他操作的组合时,避免命名中间值。
为什么我要使用不是 monad 的应用程序?
由于 applicatives 比 monads 更受限制,这意味着您可以提取有关它们的更多有用的静态信息。
这方面的一个例子是应用解析器。monadic 解析器支持使用顺序组合(>>=) :: Monad m => m a -> (a -> m b) -> m b
,而应用程序解析器仅使用(<*>) :: Applicative f => f (a -> b) -> f a -> f b
. 类型使区别显而易见:在单子解析器中,语法可以根据输入而改变,而在应用程序解析器中,语法是固定的。
通过以这种方式限制接口,例如,我们可以确定解析器是否会在不运行空字符串的情况下接受它。我们还可以确定 first 和 follow 集合,它们可以用于优化,或者,正如我最近一直在玩的那样,构建支持更好的错误恢复的解析器。