我是一名 Scala 程序员,现在正在学习 Haskell。很容易找到 OO 概念的实际用例和现实世界的示例,例如装饰器、策略模式等。书籍和互联网上到处都是。
我意识到这在某种程度上不是功能概念的情况。恰当的例子:应用程序。
我正在努力寻找应用程序的实际用例。到目前为止,我遇到的几乎所有教程和书籍都提供了 []
和 Maybe
的示例。我希望 applicatives 比这更适用,看到他们在 FP 社区中获得的所有关注。
我想我理解了applicatives 的概念基础(也许我错了),我已经等了很久才得到启蒙。但它似乎没有发生。在编程时,我从来没有过高兴地大喊“Eureka!我可以在这里使用applicative!”的时刻。 (除了 []
和 Maybe
)。
有人可以指导我如何在日常编程中使用应用程序吗?我如何开始发现模式?谢谢!
Applicative
如何成为迭代器模式的本质的全部内容。
当你有一个由多个变量组成的普通旧函数并且你有参数但它们被包裹在某种上下文中时,应用程序很棒。例如,您有普通的旧连接函数 (++)
,但您想将其应用于通过 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
只能提升一元函数。
由于许多应用程序也是单子,我觉得这个问题确实有两个方面。
当两者都可用时,为什么我要使用应用程序接口而不是单子接口?
这主要是风格问题。尽管 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 集合,它们可以用于优化,或者,正如我最近一直在玩的那样,构建支持更好的错误恢复的解析器。
[Foo x y | x <- randomIO, y <- randomIO]
我认为 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
我终于明白了应用程序如何通过该演示文稿帮助日常编程:
作者展示了应用程序如何帮助组合验证和处理失败。
演示文稿使用 Scala,但作者还提供了 Haskell、Java 和 C# 的完整代码示例。
警告:我的回答是说教/道歉。所以起诉我。
那么,在您的日常 Haskell 编程中,您多久创建一次新的数据类型?听起来您想知道何时创建自己的 Applicative 实例,老实说,除非您正在滚动自己的解析器,否则您可能不需要做太多事情。另一方面,使用应用实例,你应该学会经常做。
Applicative 不是像装饰器或策略那样的“设计模式”。它是一种抽象,这使得它更加普遍和普遍有用,但更不那么有形。您很难找到“实际用途”的原因是因为示例使用它几乎太简单了。您使用装饰器在窗口上放置滚动条。您使用策略来统一您的国际象棋机器人的攻击性和防御性移动界面。但是应用程序有什么用?嗯,它们更通用,所以很难说它们是干什么用的,没关系。应用程序作为解析组合器很方便; Yesod Web 框架使用 Applicative 来帮助设置和从表单中提取信息。如果你看,你会发现 Applicative 的用途有一百万零一个;到处都是。但由于它是如此抽象,你只需要感受它,以便识别它可以帮助你的生活更轻松的许多地方。
[]
和 Maybe
实例,人们也会对 Applicative
的形状和使用方式有所了解。这就是使任何类型类有用的原因:不一定要确切地知道每个实例的作用,而是大致了解 Applicative 组合器的作用,因此当您遇到新的数据类型时,您会发现它有一个 Applicative 实例,您可以立即开始使用它。
我认为 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
默认不是中缀运算符。所以它必须更像这样:fmap (toGregorian . utctDay) getCurrentTime
fmap
的问题在于,当您想将多个参数的普通函数应用于多个一元值时,它不起作用;解决这个问题就是 Applicative
正确的地方。
来自“Functor”的 Applicative,它概括了“fmap”以轻松表达对多个参数(liftA2)或一系列参数(使用 <*>)的操作。
来自“Monad”的 Applicative 不会让计算依赖于计算的值。具体来说,您不能对返回值进行模式匹配和分支,通常您所能做的就是将其传递给另一个构造函数或函数。
因此,我认为 Applicative 夹在 Functor 和 Monad 之间。识别出你何时不在单子计算的值上进行分支是查看何时切换到 Applicative 的一种方法。
这是从 aeson 包中获取的示例:
data Coord = Coord { x :: Double, y :: Double }
instance FromJSON Coord where
parseJSON (Object v) =
Coord <$>
v .: "x" <*>
v .: "y"
有一些像 ZipList 这样的 ADT 可以有应用实例,但不能有单子实例。在理解应用程序和单子之间的区别时,这对我来说是一个非常有用的例子。由于这么多应用程序也是单子,如果没有像 ZipList 这样的具体示例,很容易看不出两者之间的区别。
我认为浏览 Hackage 上的包源,并亲眼看看现有的 Haskell 代码中如何使用 applicative functor 等可能是值得的。
我在讨论中描述了应用函子的实际使用示例,我在下面引用。
请注意,代码示例是我的假设语言的伪代码,它将以子类型的概念形式隐藏类型类,因此如果您看到 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# 特定于案例的样板将随着程序的增长而呈几何级数增长。
(| (++) getLine getLine |)
中,两个getLine
操作的排序对结果很重要......(<*>)
对事物进行排序是任意的,但按照惯例通常是从左到右的,例如f <$> x <*> y
==do { x' <- x; y' <- y; return (f x y) }
Monad
实例,(<*>)
=ap
,它修复了排序以匹配我上面的示例。