ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

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

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

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

我第一次受到这两篇文章的启发学习这些东西:debasishg.blogspot.com/2010/11/exploring-scalaz.html debasishg.blogspot.com/2011/02/…
论文 The Essence of the Iterator Pattern 是关于 Applicative 如何成为迭代器模式的本质的全部内容。

d
duplode

当你有一个由多个变量组成的普通旧函数并且你有参数但它们被包裹在某种上下文中时,应用程序很棒。例如,您有普通的旧连接函数 (++),但您想将其应用于通过 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 只能提升一元函数。


我不确定 applicative IO 示例是不是一个好的示例,因为 applicative 不太关心排序恕我直言,但在 (| (++) getLine getLine |) 中,两个 getLine 操作的排序对结果很重要......
@hvr:哪个顺序 (<*>) 对事物进行排序是任意的,但按照惯例通常是从左到右的,例如 f <$> x <*> y == do { x' <- x; y' <- y; return (f x y) }
@hvr:好吧,请记住,表达式本身 can't 依赖于排序,因为提升的函数无法观察到差异,无论如何都会发生两种效果。选择哪个顺序由实例单独定义,它应该知道哪个是正确的。另外,请注意文档指定对于 Monad 实例,(<*>) = ap,它修复了排序以匹配我上面的示例。
<$> 和 <*> 样式运算符被声明为“infixl 4”,因此没有歧义的约定,它通过声明从左到右分组/关联来指定。 r2l 或 l2r 的效果顺序仍然由实际实例控制,对于 monads,它使用与“Control.Monad.ap”相同的顺序,即“liftM2 id”,并且 LiftM2 被记录为从左到右运行。
@Chris,从左到右分组与从左到右执行没有任何关系。
d
duplode

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

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

这主要是风格问题。尽管 monad 具有 do-notation 的语法糖,但使用 applicative 样式通常会导致更紧凑的代码。

在这个例子中,我们有一个类型 Foo,我们想要构造这种类型的随机值。使用 IO 的 monad 实例,我们可以写

data Foo = Foo Int Double

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

applicative 变体要短得多。

randomFoo = Foo <$> randomIO <*> randomIO

当然,我们可以使用 liftM2 来获得类似的简洁性,但是 applicative 风格比必须依赖于特定于 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 集合,它们可以用于优化,或者,正如我最近一直在玩的那样,构建支持更好的错误恢复的解析器。


iinm,最近在 ghc 中重新添加的 monad 推导提供了与应用组合器几乎相同级别的紧凑性:[Foo x y | x <- randomIO, y <- randomIO]
@Dan:这肯定比“做”的例子要短,但它仍然不是无点的,这在 Haskell 世界中似乎是可取的
I
IttayD

我认为 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


这是介绍 Functor、Applicatives 和 Monads 的一种有趣方式,非常值得一篇完整的博客文章展示“等...”背后的细节。
截至今天,链接似乎已断开。 Wayback 机器链接是 web.archive.org/web/20140604075710/http://www.tikalk.com/…
M
Max

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

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

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

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


不幸的是,该链接已损坏。
D
Dan Burton

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

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

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


我很惊讶这个答案收到了一个复选标记,而其他几个答案(例如 hammar 和 Oliver 的答案)却在页面的下方。我建议这些是优越的,因为它们提供了 Maybe 和 [] 之外的优秀应用程序示例。告诉提问者更深入地思考根本没有帮助。
@darrint - 显然提问者确实觉得它很有帮助,因为他是那个将它标记为已接受的人。我坚持我所说的:如果花时间玩弄,即使只有 []Maybe 实例,人们也会对 Applicative 的形状和使用方式有所了解。这就是使任何类型类有用的原因:不一定要确切地知道每个实例的作用,而是大致了解 Applicative 组合器的作用,因此当您遇到新的数据类型时,您会发现它有一个 Applicative 实例,您可以立即开始使用它。
d
duplode

我认为 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

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


实际上,<$> 只是 fmap,它是从 Data.Functor 重新导出的。
@Sjoerd Visscher:正确... <$> 的使用仍然更具吸引力,因为 fmap 默认不是中缀运算符。所以它必须更像这样:fmap (toGregorian . utctDay) getCurrentTime
fmap 的问题在于,当您想将多个参数的普通函数应用于多个一元值时,它不起作用;解决这个问题就是 Applicative 正确的地方。
@oliver 我认为 Sjoerd 的意思是,您所展示的并不是应用程序有用的示例,因为您实际上只是在处理函子。它确实展示了应用风格是如何有用的。
C
Chris Kuklewicz

来自“Functor”的 Applicative,它概括了“fmap”以轻松表达对多个参数(liftA2)或一系列参数(使用 <*>)的操作。

来自“Monad”的 Applicative 不会让计算依赖于计算的值。具体来说,您不能对返回值进行模式匹配和分支,通常您所能做的就是将其传递给另一个构造函数或函数。

因此,我认为 Applicative 夹在 Functor 和 Monad 之间。识别出你何时不在单子计算的值上进行分支是查看何时切换到 Applicative 的一种方法。


d
duplode

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

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

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

S
Sukant Hajra

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


A
Artyom Shalkhakov

我认为浏览 Hackage 上的包源,并亲眼看看现有的 Haskell 代码中如何使用 applicative functor 等可能是值得的。


一个特定的链接或更多的细节都值得在这里添加。
S
Shelby Moore III

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

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

如果我们用 null 或 none 标记数组或 hashmap 的元素以指示它们的索引或键是有效但无价值的,则 Applicative 无需任何样板即可在将操作应用于具有值的元素时跳过无价值元素。更重要的是,它可以自动处理任何先验未知的 Wrapped 语义,即 T over Hashmap[Wrapped[T]] 上的操作(任何组合级别的操作,例如 Hashmap[Wrapped[Wrapped2[T]]],因为 applicative 是可组合但 monad 不是)。我已经可以想象它将如何使我的代码更易于理解。我可以专注于语义,而不是让我到达那里的所有麻烦,并且我的语义将在 Wrapped 的扩展下开放,而您的所有示例代码都不是。值得注意的是,我之前忘记指出,您之前的示例没有模拟 Applicative 的返回值,它将是一个 List,而不是 Nullable、Option 或 Maybe。因此,即使我尝试修复您的示例也没有模仿 Applicative.apply。请记住 functionToApply 是 Applicative.apply 的输入,因此容器保持控制。 list1.apply( list2.apply( ... listN.apply( List.lift(functionToApply) ) ... ) ) 等价。 list1.apply( list2.apply( ... listN.map(functionToApply) ... ) ) 以及我建议的语法糖,编译器会将其转换为上述内容。 funcToApply(list1, list2, ... 列表 N)

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

大多数程序员可能不希望将语句外控制流与赋值相结合 Applicative.apply 用于在类型参数的任何嵌套(组合)级别将函数的部分应用推广到参数化类型(又名泛型)。这一切都是为了使更通用的组合成为可能。将其拉到函数的已完成求值(即返回值)之外是无法实现通用性的,类似于洋葱不能由内而外剥。因此,这不是混淆,它是一种新的自由度,您当前无法使用。根据我们的讨论线程,这就是为什么您必须抛出异常或将它们存储在全局变量中的原因,因为您的语言没有这种自由度。这不是这些范畴论函子的唯一应用(在我在主持人队列中的评论中阐述)。我提供了一个在 Scala、F# 和 C# 中抽象验证的示例的链接,该示例目前卡在主持人队列中。比较令人讨厌的 C# 版本的代码。原因是因为 C# 没有通用化。我直观地期望 C# 特定于案例的样板将随着程序的增长而呈几何级数增长。