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


71

我是Scala程序员,现在正在学习Haskell。很容易找到面向对象概念的实际用例和现实示例,例如装饰器,策略模式等。书和互连网充满了它。

我意识到功能性概念并非如此。恰当的例子:应用剂

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

我认为我理解了应用程序的概念基础(也许我错了),并且我一直在等待启蒙的时刻。但这似乎没有发生。从来没有在编程时,我有一阵子会高兴地喊着:“尤里卡!我可以在这里使用应用程序!” (同样,for[]和除外Maybe)。

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


1
我的灵感来自于这两篇文章来学习这些东西第一次:debasishg.blogspot.com/2010/11/exploring-scalaz.html debasishg.blogspot.com/2011/02/...
CheatEx



本文迭代器模式的本质是怎么一回事Applicative是迭代器模式的精髓。
罗素·奥康纳

Answers:


11

警告:我的回答是布道/道歉。告我

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

适用性不是装饰器或策略之类的“设计模式”。它是一种抽象,使它更加普及和普遍有用,但无形得多。您很难找到“实际用途”的原因是因为该示例的用途几乎太简单了。您可以使用装饰器在窗口上放置滚动条。您可以使用策略统一国际象棋机器人进取和防御动作的界面。但是,什么是应用?好吧,它们的通用性更高,因此很难说出它们的用途,这没关系。应用程序很容易用作解析组合器。Yesod Web框架使用Applicative来帮助设置和从表单中提取信息。如果您看一下,就会发现有100万个用途可用于Applicative;到处都是。但既然如此


19
令我惊讶的是,这个答案收到了一个对勾,而其他几个答案(例如hammar和Oliver的答案)都远远低于页面。我建议它们是优越的,因为它们提供了Maybe和[]以外的应用实例。告诉发问者更深入的思考根本没有帮助。
darrint 2012年

1
@darrint-显然,发问者的确对它有帮助,因为他是将其标记为已接受的人。我坚持我所说的:如果一个人花时间在游戏中,甚至在正义[]Maybe实例的陪伴下,人们都会对形状Applicative和使用方式有一种感觉。这就是使任何类型类有用的原因:不必确切地知道每个实例的作用,而是大致了解应用组合器的一般功能,因此当您遇到新的数据类型时,就会知道它具有一个应用实例,您可以立即开始使用它。
丹·伯顿

72

当您拥有几个变量的简单旧功能并且有参数时,应用程序就很棒了,但是它们被包裹在某种上下文中。例如,您具有普通的旧串联函数,(++)但您想将其应用于通过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功能的额外功能是它可以解除任意Arity的功能,而fmap只能解除一元功能。


2
我不确定适用的IO示例是否是一个很好的例子,因为适用的IO并不是很在乎排序的恕我直言,但是(| (++) getLine getLine |)两个getLine操作的排序对于结果来说变得很重要...
HVR

2
@hvr:按(<*>)什么顺序排列事物是任意的,但通常按惯例从左到右,例如f <$> x <*> y==do { x' <- x; y' <- y; return (f x y) }
CA McCann

2
@hvr:好的,请记住,表达式本身不能依赖于排序,因为提升的函数无法观察到差异,并且无论如何都会产生两种效果。选择哪个顺序仅由实例定义,哪个实例应该知道哪个正确。另外,请注意,文档为Monad实例指定(<*>)= ap,它可以固定顺序以匹配上面的示例。
CA McCann

1
<$>和<*>样式运算符被声明为“ infixl 4”,因此没有模棱两可的约定,它通过声明将从左到右进行分组/关联来指定。效果的r2l或l2r顺序仍由实际实例控制,对于monad,其使用与“ Control.Monad.ap”相同的顺序,即“ liftM2 id”,并且记录了liftM2从左到右运行。
克里斯·库克莱维奇

1
@Chris,从左到右分组与执行从左到右无关。
Rotsor 2011年

51

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

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

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

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

data Foo = Foo Int Double

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

适用的变体要短得多。

randomFoo = Foo <$> randomIO <*> randomIO

当然,我们可以使用它liftM2来获得类似的简洁性,但是应用样式比必须依赖于特定于Arity的提升功能更为简洁。

在实践中,我通常会以与使用无点样式相同的方式使用应用程序:当一个操作更清楚地表示为其他操作的组合时,避免命名中间值。

我为什么要使用非monad的应用程序?

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

应用解析器就是一个例子。monadic解析器支持顺序组合使用(>>=) :: Monad m => m a -> (a -> m b) -> m b,而应用解析器仅使用(<*>) :: Applicative f => f (a -> b) -> f a -> f b。类型使区别显而易见:在单子语法分析器中,语法可以根据输入而变化,而在应用语法分析器中,语法是固定的。

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


4
iinm,ghc中最近重新添加的monad理解力提供了与应用组合器几乎相同的压缩程度:[Foo x y | x <- randomIO, y <- randomIO]
Dan Burton

3
@丹:这肯定比“做”的例子短,但它仍然不是没有意义的,这在Haskell的世界中似乎是可取的
Jared Updike

16

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

假设您要编写一个Future [T]类。即,持有要计算的值的类。

在Java思维方式中,您可以像这样创建它

trait Future[T] {
  def get: T
}

“获取”在哪里阻止,直到该值可用为止。

您可能意识到这一点,并将其重写以进行回调:

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。

一旦沿着这条路开始,就无法在此停下来。您意识到,要操纵两个期货,您只需要建模为可应用程序,创建期货,就需要一个monad定义期货等。

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


1
这是介绍Functor,Applicatives和Monads的有趣方法,值得一整篇博客文章,其中显示“ etc ...”后面的详细信息。
艾瑞克(Eric)

截至今天,链接似乎已断开。Wayback机器链接是web.archive.org/web/20140604075710/http://www.tikalk.com/…–
superjos

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#的完整代码示例。


2
不幸的是,链接断开了。
thSoft

1

9

我认为Applicatives简化了Monadic代码的一般用法。您有几次遇到想要应用某个功能但该功能不是单调的情况,而您想将其应用于的值是单调的情况?对我来说:很多次!
这是我昨天写的一个示例:

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默认情况下它不是infix运算符。因此,它必须像这样:fmap (toGregorian . utctDay) getCurrentTime
Oliver

1
问题fmap在于,当您想将多个参数的普通函数应用于多个monadic值时,它将不起作用。解决这是在Applicative正确的用武之地
CA麦肯

2
@oliver我认为Sjoerd所说的是,所显示的内容实际上并不是应用程序有用的示例,因为您实际上只在处理函子。它确实展示了应用样式如何有用。
kqr

7

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

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

因此,我认为Applicative夹在Functor和Monad之间。识别何时不分支单子计算的值是查看何时切换为“适用”的一种方法。


5

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

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

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

4

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



1

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

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

如果我们用null或标记数组或哈希图的元素none以指示其索引或键有效但Applicative 无值,则在没有操作的情况下,启用将跳过无值元素,同时对具有值的元素进行操作。更重要的是,它可以自动处理Wrapped先验未知的任何语义,即,THashmap[Wrapped[T]](在任何级别的合成上,例如Hashmap[Wrapped[Wrapped2[T]]]由于应用性是组合的,但单子是不是)。

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

显著,我忘了指出,在此之前,你之前的例子并不效仿的返回值Applicative,这将是一个 List,而不是一个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)

阅读该交互式讨论很有用,因为我不能在此处全部复制。考虑到该博客的所有者是谁,我希望该URL不会中断。例如,我在讨论中进一步引用。

大多数程序员可能不希望将声明外的控制流与赋值相结合

Applicative.apply用于在类型参数的任何嵌套(组合)级别上将函数对参数化类型(即泛型)的部分应用普遍化。这就是使更通用的组合成为可能。不能通过将其拉到函数的已完成评估(即返回值)之外来实现通用性,类似于无法从内向外剥离洋葱。

因此,它不是合并,它是您当前无法使用的新自由度。根据我们的讨论线索,这就是为什么您必须引发异常或将它们存储在全局变量中的原因,因为您的语言没有这种自由度。这不是这些类别理论函子的唯一应用(我在主持人队列中的评论中详细介绍了)。

我提供了指向Scala,F#和C#中的抽象验证示例的链接,该示例当前停留在主持人队列中。比较令人讨厌的C#版本的代码。原因是因为C#没有被泛化。我直观地期望C#特定于案例的样板将随着程序的增长而几何爆炸。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.