当具有以下签名的函数有用时,任何人都可以分享良好的现实生活情况吗?
f (a -> b) -> f a -> f b
我无法真正看到我需要的地方,例如来自learn-you-a-haskell [(+),(*)] <*> [1,2] <*> [3,4]
答案 0 :(得分:6)
应用程序可以用于很多你可能也使用monad的东西,但是并不真正需要那个类的强大功能。 (更准确地说,只要算子的“形状”不依赖于中的值,那么Applicative
就足够了。)例如,像
foobar :: IO [String]
foobar = do
fooTxt <- readFile "foo.txt"
barTxt <- readFile "bar.txt"
return $ zip (lines fooTxt) (lines barTxt)
也可以写成
foobar = zip <$> (lines <$> readFile "foo.txt")
<*> (lines <$> readFile "bar.txt")
在这种情况下,这只会使它变得更短,但在其他情况下,它也可能会提高性能(因为Applicative
不太通用,可以进行更多优化)或者允许您使代码更通用您使用Monad
界面。
答案 1 :(得分:5)
我最常见的用途是为函数使用多个Monadic(或在本例中为Applicative)值。我使用它的一个非常常见的方法是构造函数。
考虑:
randomAge :: Rand StdGen Int
randomHeight :: Rand StdGen Double
randomWeight :: Rand StdGen Double
data Person = Person { age :: Int, height :: Double, weight :: Double }
randomPerson :: Rand StdGen Person
randomPerson = Person <$> randomAge <*> randomHeight <*> randomWeight
-- If we only had Monads...
randomPerson' :: Rand StdGen Person
randomPerson' = liftM3 Person randomAge randomHeight randomWeight
-- or worse...
randomPerson'' = randomAge >>= \ra -> randomHeight >>= \rh -> randomWeight >>= \rw -> return $ Person ra rh rw
或FPComplete's tutorial on the JSON parser Aeson中的此示例:
instance FromJSON Person where
parseJSON (Object v) =
Person <$> v .: "firstName"
<*> v .: "lastName"
<*> v .: "age"
<*> v .: "likesPizza"
parseJSON _ = mzero
或Real World Haskell chapter on Parser Combinators中的此示例:
ghci> let parser = (++) <$> string "HT" <*> (string "TP" <|> string "ML")
大多数情况下,当您拥有f :: a -> b -> c
且m a
和m b
并且您需要m c
时,申请人会很有用。
编辑:那说还有其他用途。例如,Applicative可以用作修改可遍历数据结构的非常好的方法。例如。通过具有函数树将树应用于值,从而将值转换为树。或者在没有列表理解语法的情况下进行非确定性列表操作。
它可能有一些意想不到的应用程序。例如,使用->
Monad:
> (<*>) (+) (+1) 2
5
最后一个有点深奥,并且不太可能在实际环境中使用,但它表明你可以在很多方面使用Applicatives。
答案 2 :(得分:3)
它是pure
等其他非常有用的功能的基本构建块(以及liftA2
)(您可以将其视为“fmap
超过两个容器”并且不会太远离开商标):
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f fa fb = pure f <*> fa <*> fb
因此,假设您要添加两个列表xs, ys :: [Integer]
的值的所有组合,您可以这样做:
liftA2 (+) xs ys
虽然我们更常见的是写这个:
(+) <$> xs <*> ys
你感到困惑,为什么有人会写[(+),(*)] <*> [1,2] <*> [3,4]
- 我认为这是一个非常人为的例子。 99%的时间使用Applicative
,你基本上有一个函数,它接受两个或多个参数,并且希望将它应用于实现{{1}的仿函数内的值}}
查看它的一种方法是我们可以选择定义Applicative
和相关的函数,如下所示:
Applicative
但选择-- Not the real definition
class Functor f => Applicative f where
pure :: a -> f a
-- Like `fmap` but for two-argument functions:
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
-- `<*>` and `liftA2` are interdefinable, as shown further up and just here:
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
ff <*> fa = liftA2 ($) ff fa
作为类方法的原因是它可以很容易地编写<*>
,liftA2
等等:
liftA3
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f fa fb = pure f <*> fa <*> fb
liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d
liftA3 f fa fb fc = pure f <*> fa <*> fb <*> fc
liftA4 :: Applicative f => (a -> b -> c -> d -> e) -> f a -> f b -> f c -> f d -> f e
liftA4 f fa fb fc fd = pure f <*> fa <*> fb <*> fc <*> fd
类几乎是支持这种“具有多个参数函数的映射”模式所需的最小功能,这在很多情况下都是有用的。
答案 3 :(得分:1)
如左下所述,每当&#34;形状&#34;每个&#34;行动&#34;不依赖于价值&#34;生产&#34;通过前一个,Applicative
就足够了。
这可能是一种非常直观的形状。例如,>>=
的一个上下文是t >>= f
获取存储在树t
的叶子中的值,将f
应用于每个值以生成新的子树,以及移植每个子树到树上代替相应的叶子。但是,此操作对平衡树不起作用,因为没有人知道树f
每次会产生多大/多深。然而,<*>
可能仍然有意义,因为嫁接树将具有相同的形状,保持平衡。