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

我是一个 Scala 程序员,正在学习 Haskell。很容易找到面向对象概念的实际用例和现实示例,如修饰符、策略模式等。书籍和网络上到处都是。

我意识到函数概念不是这样的。

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

我想我明白了 应用程序的概念基础(也许我错了) ,我已经等待了很长时间的启蒙时刻。但似乎不可能。在编程的时候,我从来没有过这样的时刻,我会高兴地大喊: “我找到了!我可以在这里使用应用程序!”(除了 []Maybe)。

有没有人能告诉我应用程序是如何在日常编程中使用的?我怎么才能找到规律呢?谢谢!

8080 次浏览

我终于明白了应用程序是如何帮助我们进行日常编程的:

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

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

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

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

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

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

与此相比,使用 Applicative:

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

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

下面是 Eason 软件包中的一个例子:

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


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

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

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

这主要是一个风格问题。虽然单子有 do符号的语法糖,但使用应用风格经常导致更紧凑的代码。

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

data Foo = Foo Int Double


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

应用变体相当短。

randomFoo = Foo <$> randomIO <*> randomIO

当然,我们可以使用 liftM2来获得类似的简洁性,但是应用风格要比依赖于特定的提升功能更简洁。

在实践中,我发现自己使用应用程序的方式与使用无点风格的方式大致相同: 当一个操作更清楚地表示为其他操作的组合时,要避免命名中间值。

我为什么要使用一个不是 monad 的应用程序?

由于应用程序比单子受到更多的限制,这意味着您可以提取关于它们的更多有用的静态信息。

这方面的一个例子是应用解析器。一元解析器支持使用 (>>=) :: Monad m => m a -> (a -> m b) -> m b的顺序组合,而应用解析器只使用 (<*>) :: Applicative f => f (a -> b) -> f a -> f b。这些类型的区别很明显: 在一元解析器中,文法可以根据输入而改变,而在应用解析器中,文法是固定的。

例如,通过以这种方式限制接口,我们可以确定解析器是否接受空字符串 而不是经营它。我们还可以确定第一个和第二个集合,这些集合可以用于优化,或者像我最近所做的那样,构造支持更好的错误恢复的解析器。

从“ Functor”到 Applicative,它将“ fmap”概括为容易表达对几个参数(liftA2)或一系列参数(使用 < * >)的行为。

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

因此,我认为应用程序夹在 Functor 和 Monad 之间。识别何时没有从一元计算中分支值是查看何时切换到 Applicative 的一种方法。

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

警告: 我的回答相当说教/道歉。所以告我吧。

那么,在您的日常 Haskell 编程中,您多久创建一次新的数据类型?听起来您想知道什么时候创建自己的 Applicative 实例,而且说实话,除非您正在运行自己的解析器,否则可能不太需要这么做。吸毒的应用实例,另一方面,你应该学会经常做。

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

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

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

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

注意,代码示例是我假设的语言的伪代码,它将隐藏类型类在一个概念形式的子类型中,所以如果你看到一个对 apply的方法调用,只需要转换成你的类型类模型,例如 Scalaz 的 <*>或者 Haskell。

如果我们使用 nullnone将数组或散列表的元素标记为 表明它们的索引或键是有效的但是没有值,则 Applicative 在没有任何样板跳过无价值的元素的情况下启用 对具有值的元素应用操作等等 重要的是,它可以自动处理任何 Wrapped语义 是未知的先验,即操作对 T超过 Hashmap[Wrapped[T]](任何级别的组合,例如 Hashmap[Wrapped[Wrapped2[T]]],因为 application 是可组合的,而 monad 不是)。

我已经可以想象它将如何使我的代码更容易 理解。我可以专注于语义,而不是所有的 我的语义将在 而所有示例代码都没有包装。

值得注意的是,我之前忘了指出你之前的例子 不要模拟 Applicative的返回值,它将是一个 List,而不是 NullableOption,或者 Maybe。所以即使我尝试 修复你的例子没有仿效 Applicative.apply

记住 functionToApplyApplicative.apply,因此容器保持控制。

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

差不多。

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

我提出的语法糖,编译器会翻译 以上。

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

读取 互动讨论非常有用,因为我无法在这里完全复制它。我希望这个网址不会被打破,考虑到谁是这个博客的主人。例如,我引用下面的讨论。

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

Apply 用于将函数的部分应用程序推广到类型参数的任何嵌套(组合)级别的参数化类型(也称为泛型)。这一切都是为了使更广义的组合成为可能。通用性不能通过将它从函数的完整求值(即返回值)之外拉出来来实现,就像洋葱不能从内向外剥一样。

因此,它不是合并,它是一个新的自由度,目前还没有提供给你。根据我们的讨论,这就是为什么必须抛出异常或将它们存储在全局变量中的原因,因为您的语言没有这种自由度。这并不是这些范畴理论函子的唯一应用(详见我在版主队列中的评论)。

我在 Scala、 F # 和 C # 中提供了一个到 一个例子抽象验证的链接,这个链接目前停留在版主队列中。比较讨厌的 C # 版本的代码。这是因为 C # 不是泛化的。我直觉地认为,随着程序的增长,C # 特定于案例的样板文件将以几何形式爆炸。