72

我是一名 Scala 程序员,现在正在学习 Haskell。很容易找到 OO 概念的实际用例和现实世界的示例,例如装饰器、策略模式等。书籍和互联网上到处都是。

我意识到这在某种程度上不是功能概念的情况。恰当的例子:应用程序

我正在努力寻找应用程序的实际用例。到目前为止,我遇到的几乎所有教程和书籍都提供了[]和的示例Maybe。我希望 applicatives 比这更适用,看到他们在 FP 社区中获得的所有关注。

我想我理解了applicatives的概念基础(也许我错了),我已经等待了很长时间的启蒙时刻。但它似乎没有发生。在编程时,我从来没有过高兴地大喊“Eureka!我可以在这里使用applicative!”的时刻。(同样,for[]和除外Maybe)。

有人可以指导我如何在日常编程中使用应用程序吗?我如何开始发现模式?谢谢!

4

11 回答 11

74

当您有几个变量的普通旧函数并且您有参数但它们被包裹在某种上下文中时,应用程序很棒。例如,您有普通的旧连接函数(++),但您想将其应用于通过 I/O 获取的 2 个字符串。然后是一个应用函子的事实IO来拯救:

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只能提升一元函数。

于 2011-08-18T08:03:16.807 回答
54

由于许多应用程序也是单子,我觉得这个问题确实有两个方面。

当两者都可用时,为什么我要使用应用程序接口而不是单子接口?

这主要是风格问题。尽管 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 集合,它们可以用于优化,或者,正如我最近一直在玩的那样,构建支持更好的错误恢复的解析器。

于 2011-08-18T13:52:17.440 回答
16

我认为 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 内部的计算会发生什么?或者,如果您有两个期货,并且您需要根据它们将提供的值进行计算,您会怎么做?

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

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

现在您的应用程序发生了变化,因此每次您需要处理包含的值时,您只需返回一个新的 Future。

一旦你从这条路开始,你就不能停在那里。你意识到为了操纵两个未来,你只需要建模为一个应用程序,为了创建未来,你需要一个未来的单子定义,等等。

更新:正如@Eric 所建议的,我写了一篇博文:http ://www.tikalk.com/incubator/blog/functional-programming-scala-rest-us

于 2011-08-21T10:54:23.420 回答
14

我终于明白了应用程序如何通过该演示文稿帮助日常编程:

https://web.archive.org/web/20100818221025/http://applicative-errors-scala.googlecode.com/svn/artifacts/0.6/chunk-html/index.html

作者展示了应用程序如何帮助组合验证和处理失败。

演示文稿使用 Scala,但作者还提供了 Haskell、Java 和 C# 的完整代码示例。

于 2011-08-18T07:55:44.407 回答
11

警告:我的回答是说教/道歉。所以起诉我。

那么,在您的日常 Haskell 编程中,您多久创建一次新的数据类型?听起来您想知道何时创建自己的 Applicative 实例,老实说,除非您正在滚动自己的解析器,否则您可能不需要做太多事情。另一方面,使用应用实例,你应该学会经常做。

Applicative 不是像装饰器或策略那样的“设计模式”。它是一种抽象,这使得它更加普遍和普遍有用,但更不那么有形。您很难找到“实际用途”的原因是因为示例使用它几乎太简单了。您使用装饰器在窗口上放置滚动条。您使用策略来统一您的国际象棋机器人的攻击性和防御性移动界面。但是应用程序有什么用?嗯,它们更通用,所以很难说它们是干什么用的,没关系。应用程序作为解析组合器很方便;Yesod Web 框架使用 Applicative 来帮助设置和从表单中提取信息。如果你看,你会发现 Applicative 的用途有一百万零一个;到处都是。但既然'

于 2011-08-19T17:35:12.670 回答
9

我认为 Applicatives 简化了单子代码的一般用法。有多少次你想应用一个函数但这个函数不是一元的,而你想应用它的值是一元的?对我来说:很多次!
这是我昨天刚写的一个例子:

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

与使用 Applicative 相比:

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

这种形式看起来“更自然”(至少在我看来:)

于 2011-08-18T08:30:40.333 回答
7

Coming at Applicative from "Functor" it generalizes "fmap" to easily express acting on several arguments (liftA2) or a sequence of arguments (using <*>).

Coming at Applicative from "Monad" it does not let the computation depend on the value that is computed. Specifically you cannot pattern match and branch on a returned value, typically all you can do is pass it to another constructor or function.

Thus I see Applicative as sandwiched in between Functor and Monad. Recognizing when you are not branching on the values from a monadic computation is one way to see when to switch to Applicative.

于 2011-08-18T13:57:32.030 回答
5

这是从 aeson 包中获取的示例:

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

instance FromJSON Coord where
   parseJSON (Object v) = 
      Coord <$>
        v .: "x" <*>
        v .: "y"
于 2011-08-18T08:53:22.427 回答
4

有一些像 ZipList 这样的 ADT 可以有应用实例,但不能有单子实例。在理解应用程序和单子之间的区别时,这对我来说是一个非常有用的例子。由于这么多应用程序也是单子,如果没有像 ZipList 这样的具体示例,很容易看不出两者之间的区别。

于 2012-02-09T23:29:14.363 回答
2

我认为浏览 Hackage 上的包源可能是值得的,并亲眼目睹应用函子等如何在现有的 Haskell 代码中使用。

于 2011-08-19T04:12:01.713 回答
1

我在讨论中描述了应用函子的实际使用示例,我在下面引用。

请注意,代码示例是我的假设语言的伪代码,它将以子类型的概念形式隐藏类型类,因此如果您看到方法调用apply只需转换为您的类型类模型,例如<*>在 Scalaz 或 Haskell 中。

如果我们将数组或哈希映射的元素标记为nullnone指示它们的索引或键是有效但Applicative 无值的,则在对具有值的元素应用操作时,无需任何样板即可跳过无值元素。更重要的是,它可以自动处理任何Wrapped先验未知的语义,即对Tover 的操作Hashmap[Wrapped[T]](任何对任何组合级别的操作,例如Hashmap[Wrapped[Wrapped2[T]]]因为 applicative 是可组合的,但 monad 不是)。

我已经可以想象它将如何使我的代码更易于理解。我可以专注于语义,而不是让我到达那里的所有麻烦,并且我的语义将在 Wrapped 的扩展下开放,而您的所有示例代码都不是。

重要的是,我之前忘记指出,您之前的示例没有模拟 的返回值Applicative,它将是 a List,而不是 a NullableOptionMaybe。因此,即使我尝试修复您的示例也没有效仿Applicative.apply

记住functionToApply是 的输入 Applicative.apply,所以容器保持控制。

list1.apply( list2.apply( ... listN.apply( List.lift(functionToApply) ) ... ) )

等价。

list1.apply( list2.apply( ... listN.map(functionToApply) ... ) )

以及我建议的语法糖,编译器会将其转换为上述内容。

funcToApply(list1, list2, ... list N)

阅读该交互式讨论很有用,因为我不能在这里全部复制。鉴于该博客的所有者是谁,我希望该网址不会中断。例如,我引用了进一步的讨论。

大多数程序员可能不希望将语句外控制流与赋值混为一谈

Applicative.apply 用于在类型参数的任何嵌套(组合)级别将函数的部分应用推广到参数化类型(又名泛型)。这一切都是为了使更通用的组合成为可能。将其拉到函数的已完成求值(即返回值)之外是无法实现通用性的,类似于洋葱不能由内而外剥。

因此,这不是混淆,它是一种新的自由度,您当前无法使用。根据我们的讨论线程,这就是为什么您必须抛出异常或将它们存储在全局变量中的原因,因为您的语言没有这种自由度。这不是这些范畴论函子的唯一应用(在我在主持人队列中的评论中阐述)。

我提供了一个在 Scala、F# 和 C# 中抽象验证的示例的链接,该示例目前卡在主持人队列中。比较令人讨厌的 C# 版本的代码。原因是因为 C# 没有通用化。我直观地期望 C# 特定于案例的样板将随着程序的增长而呈几何级数增长。

于 2013-03-14T07:51:41.877 回答