应用风格的实际用途是什么?

75

我是一名Scala程序员,现在正在学习Haskell。容易找到面向对象(OO)概念的实际用例和真实世界的示例,例如装饰器、策略模式等,书籍和网络上充斥着这些内容。

但我意识到函数式概念似乎并非如此。以applicatives为例。

我很难找到applicatives的实际用例。迄今为止,我遇到的几乎所有教程和书籍都提供了[]Maybe的例子。考虑到在FP社区中它们受到的关注,我原本期望applicatives具有更广泛的适用性。

我认为我理解了applicatives的概念基础(也许我错了),我已经等待了很长时间,希望能有启蒙的时刻。但似乎并未发生。除了[]Maybe之外,我从未在编程时有过“欣喜若狂的时刻”(感叹我可以在这里使用applicative!)。

请问有人能指导我如何在日常编程中使用applicatives吗?我该如何开始发现模式?谢谢!


1
我第一次被激发学习这些东西是通过这两篇文章: http://debasishg.blogspot.com/2010/11/exploring-scalaz.html http://debasishg.blogspot.com/2011/02/applicatives-for-composable-json.html - CheatEx
请仅返回翻译后的文本:紧密相关:https://dev59.com/jXI95IYBdhLWcg3w2h56 - Mauricio Scheffer
3
https://groups.google.com/forum/#!msg/scala-user/uh5w6N2eAHY/3Shf1295VpYJ - Mauricio Scheffer
这篇论文迭代器模式的本质讲述了Applicative是迭代器模式的本质。 - Russell O'Connor
11个回答

77

当你有一个多个变量的普通函数,而且你已经拥有了这些变量作为参数但它们被某种上下文包裹时,Applicatives非常实用。例如,你有一个普通的连接函数(++),但你想将其应用于通过I/O获取的2个字符串。这时,IO作为一个applicative functor就能解救你了:

Prelude Control.Applicative> (++) <$> getLine <*> getLine
hi
there
"hithere"

虽然您明确要求非Maybe的示例,但我认为这是一个很好的用例,所以我会举个例子。您有一个多个变量的常规函数,但您不知道是否拥有您需要的所有值(其中一些可能未能计算,导致Nothing)。因此,由于您拥有“部分值”,您希望将函数转换为部分函数,如果其任何输入未定义,则该函数未定义。然后

Prelude Control.Applicative> (+) <$> Just 3 <*> Just 5
Just 8

但是

Prelude Control.Applicative> (+) <$> Just 3 <*> Nothing
Nothing

这正是您想要的。

基本思想是将普通函数“提升”到一个上下文中,以便可以将其应用于尽可能多的参数。 Applicative 比基本的 Functor 更强大的额外功能在于它可以提升任意元数的函数,而 fmap 只能提升一元函数。


2
我不确定applicative IO的例子是否合适,因为在我看来,applicative并不太关心顺序,但是在(| (++) getLine getLine |)中,两个getLine操作的顺序对结果变得重要... - hvr
2
@hvr:(<*>) 运算符的顺序是任意的,但通常按照从左到右的约定进行排序,因此 f <$> x <*> y == do { x' <- x; y' <- y; return (f x y) } - C. A. McCann
2
@hvr:请记住,在表达式本身中,不能依赖于顺序,因为提升的函数无法观察到差异,无论选择哪一种效果都会发生。选择哪个顺序由实例独自定义,应该知道哪一个是正确的。此外,请注意,文档规定对于Monad实例,(<*>) = ap,这将修复排序以匹配我上面的示例。 - C. A. McCann
1
<$>和<*>风格的运算符被声明为"infixl 4",因此没有歧义的约定,它在声明中指定了从左到右的分组/关联。r2l或l2r效果顺序仍由实际实例控制,对于单子使用与"Control.Monad.ap"相同的顺序,即"liftM2 id",并且liftM2的文档记录为从左到右运行。 - Chris Kuklewicz
1
@Chris,从左到右分组并不意味着从左到右执行。 - Rotsor
显示剩余2条评论

55

由于许多applicative也是monad,我觉得这个问题实际上有两个方面。

既然两者都可用,为什么要使用applicative接口而不是monadic接口?

这主要取决于编程风格。虽然monad具有do-notation的语法糖,但使用applicative风格通常会导致更紧凑的代码。

在这个例子中,我们有一个类型Foo,并且我们想构造这个类型的随机值。使用IO的monad实例,我们可能会写成:

data Foo = Foo Int Double

randomFoo = do
    x <- randomIO
    y <- randomIO
    return $ Foo x y

应用变量更加简短。

randomFoo = Foo <$> randomIO <*> randomIO
当然,我们可以使用liftM2来获得类似的简洁性,但是applicative样式比依赖于特定元数的lifting函数更整洁。
在实践中,我发现自己主要使用applicatives的方式与我使用point-free样式的方式非常相似:当一个操作更清晰地表示为其他操作的组合时,避免命名中间值。
为什么我想使用不是monad的applicative?
由于applicatives比monads更受限制,这意味着您可以提取有关它们的更多有用的静态信息。
其中一个例子是applicative解析器。而monadic解析器支持使用(>>=) :: Monad m => m a -> (a -> m b) -> m b进行顺序组合,applicative解析器仅使用(<*>) :: Applicative f => f (a -> b) -> f a -> f b。类型使区别显而易见:在monadic解析器中,语法可以根据输入而改变,而在applicative解析器中,语法是固定的。
通过以这种方式限制接口,例如,我们可以确定解析器是否接受空字符串without running it。我们还可以确定第一个和跟随集合,这可用于优化,或者如我最近所尝试的那样,构建支持更好的错误恢复的解析器。

4
最近在 GHC 中重新添加的单子推导(monad comprehensions)可以提供与应用组合器(applicative combinators)几乎相同的紧凑性水平,例如[Foo x y | x <- randomIO, y <- randomIO] - Dan Burton
3
@Dan:这个例子比“do”示例确实更短,但它仍然不是"point-free",在Haskell世界中这似乎是理想的。 - Jared Updike

17

我认为Functor、Applicative和Monad是设计模式。

假设你想编写一个Future[T]类,这个类可以保存需要计算的值。

如果考虑使用Java的思维方式,可能会像这样创建:

trait Future[T] {
  def get: T
}

当'get'阻塞直到值可用时。

您可能已经意识到这一点,并重写它以使用回调函数:

trait Future[T] {
  def foreach(f: T => Unit): Unit
}

但是如果未来有两个用途怎么办?这意味着您需要维护一个回调列表。此外,如果一个方法接收到Future[Int]并且需要基于内部的Int返回计算结果怎么办?或者如果您有两个futures并且需要根据它们提供的值计算某些东西,该怎么办?

但是如果您了解FP概念,就知道可以操作Future实例而不是直接处理T。

trait Future[T] {
  def map[U](f: T => U): Future[U]
}

现在你的应用程序发生了变化,每次需要处理包含的值时,只需返回一个新的Future。
一旦你开始这条路,就无法停止。你会意识到,为了操作两个Future,你只需要将其建模为一个可应用程序,在创建Future时,你需要一个Future的单调定义等等。
更新:如@Eric所建议的,我写了一篇博客文章:http://www.tikalk.com/incubator/blog/functional-programming-scala-rest-us

1
这是一个有趣的介绍Functor、Applicatives和Monads的方式,值得撰写一篇完整的博客文章来展示“等等”背后的细节。 - Eric
链接似乎在今天已经失效。Wayback机器的链接是https://web.archive.org/web/20140604075710/http://www.tikalk.com/incubator/functional-programming-scala-rest-us - superjos

14

2
链接不幸地已损坏。 - thSoft
1
网络时光机链接:https://web.archive.org/web/20100818221025/http://applicative-errors-scala.googlecode.com/svn/artifacts/0.6/chunk-html/index.html - superjos

10

我认为应用函子可以简化单子代码的普遍使用。你有多少次遇到过这种情况:想要应用一个函数,但这个函数不是单子类型的,而你想要应用它的值又是单子类型的?对于我来说:这种情况非常多!
下面是我昨天刚写的一个例子:

ghci> import Data.Time.Clock
ghci> import Data.Time.Calendar
ghci> getCurrentTime >>= return . toGregorian . utctDay

与使用 Applicative 相比:

ghci> import Control.Applicative
ghci> toGregorian . utctDay <$> getCurrentTime

这个表单看起来“更自然”(至少在我看来是这样的 :)


2
实际上,<$> 只是 fmap,它被重新导出自 Data.Functor。 - Sjoerd Visscher
1
@Sjoerd Visscher:正确...使用<$>仍然更吸引人,因为fmap默认不是一个中缀运算符。所以它应该更像这样:fmap (toGregorian . utctDay) getCurrentTime - oliver
1
fmap 的问题在于,当你想要将一个多参数的普通函数应用于多个单子值时它不起作用;而解决这个问题正是 Applicative 的作用所在。 - C. A. McCann
2
@oliver 我认为Sjoerd的意思是你展示的并不是applicatives有用的一个例子,因为你只是在处理functor。但它确实展示了applicative style的有用性。 - kqr

10

警告:我的回答可能有点说教和道歉。不过你可以告我。

那么,在你日常的Haskell编程中,你有多经常创建新的数据类型?如果你想知道何时需要创建自己的Applicative实例,老实说,除非你正在编写自己的解析器,否则你可能不需要这样做太多。使用Applicative实例,则应该学会经常使用。

Applicative并不像装饰器或策略模式那样是一个“设计模式”。它是一种抽象,因此更加普遍和有用,但具体性却更少。你很难找到“实际用途”的原因是,它的示例用途几乎太简单了。你使用装饰器在窗口上放置滚动条。你使用策略来统一你的国际象棋机器人的攻击和防御移动接口。但是,什么是Applicatives呢?好吧,它们更加泛化,所以很难说它们是为什么而存在,这没关系。Applicatives作为解析组合器非常方便;Yesod web框架使用Applicative来帮助设置和提取表单信息。如果你仔细寻找,你会发现Applicative有成千上万种用法;它无处不在。但是,由于它太过抽象,你只需要感觉到它,就能认识到它可以帮助你更轻松地生活的许多地方。


20
我很惊讶这个答案获得了勾选,而其他几个答案(例如hammar和oliver的答案)则排在很下面。我认为那些答案更优秀,因为它们提供了除了Maybe和[]之外的Applicative的极好例子。只是告诉提问者要深入思考并没有什么帮助。 - darrint
1
@darrint - 显然,提问者认为这很有帮助,因为他是将其标记为已接受的人。我坚持我的观点:如果一个人花时间玩耍,即使只是使用 []Maybe 实例,他也会对 Applicative 的形状和如何使用它有所了解。这就是任何类型类有用的地方:不一定要知道每个实例确切的作用,而是要对 Applicative 组合器的一般作用有一个大致的了解,这样当你遇到一个新的数据类型,并且你学习到它有一个 Applicative 实例时,你可以立即开始使用它。 - Dan Burton

7
从“Functor”的角度来看,Applicative将“fmap”泛化为轻松表达对多个参数(liftA2)或一系列参数(using <*>)进行操作。
从“Monad”的角度来看,它不允许计算依赖于所计算的值。具体而言,您不能对返回的值进行模式匹配和分支,通常只能将其传递给另一个构造函数或函数。
因此,我认为Applicative夹在Functor和Monad之间。识别何时不要在单调计算中分支是切换到Applicative的一种方法。

5

以下是aesons程序包中的示例:

data Coord = Coord { x :: Double, y :: Double }

instance FromJSON Coord where
   parseJSON (Object v) = 
      Coord <$>
        v .: "x" <*>
        v .: "y"

4

有一些ADT(抽象数据类型)例如ZipList可以有应用实例,但不能有单子实例。这对我理解应用和单子之间的区别非常有帮助。由于许多应用也是单子,没有像ZipList这样的具体示例很容易看不出两者之间的区别。


2

我认为浏览Hackage上软件包的源代码可能是值得的,可以直接了解到在现有的Haskell代码中如何使用applicative functors等技术。


2
在这里添加一个具体的链接或更多详细信息都是值得的。 - Vlad Patryshev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接