可应用函子

6
我有些困难理解 Haskell 中 Applicative 的函数实例 (->) r 是如何工作的。
例如,如果我有:
(+) <$> (+3) <*> (*100) $ 5

我知道你得到了结果508,我有点理解你是将(5+3)(5*100)的结果分别应用(+)函数,但我并没有完全理解发生了什么。我猜测表达式被括在以下位置:
((+) <$> (+3)) <*> (*100)

据我理解,发生的情况是您在最终结果上映射(+),然后使用<*>运算符将该函数应用于(*100)的最终结果。
但是,我不理解(->)r实例的<*>实现,也不明白为什么我不能这样写:
(+3) <*> (*100)

这句话的意思是:“当涉及到(->)r时,<*>、<$>运算符如何工作?”

1
我相信你忘记加一个 <$>(+) <$> (+3) <*> (*100) $ 5。它的解析方式应为 (((+) <$> (+3)) <*> (*100)) $ 5 - Daniel Wagner
@DanielWagner,由于Markdown解析器的原因,<$>被吃掉了(但是<*>没有)。我已经添加了必要的代码转义。 - Alexis King
啊,我明白了,谢谢你。我不知道你必须转义 <$> - Zubair
5个回答

9

<$> 只是 fmap 的另一个名称,对于 (->) r 的定义是 (.)(组合运算符):

intance Functor ((->) r) where
  fmap f g = f . g

你基本上可以通过查看类型来推断出 <*> 的实现:
instance Applicative ((->) r) where
  (<*>) :: (r -> a -> b) -> (r -> a) -> (r -> b)
  f <*> g = \x -> f x (g x)

你有一个从rab的函数,还有一个从ra的函数。你想要得到一个从rb的函数作为结果。首先你需要返回一个函数:
\x ->

现在您想应用f,因为它是唯一可能返回b的项:
\x -> f _ _

现在,f 的参数的类型是 ra。因为 r 已经是 x 的类型了,你可以通过将 x 应用于 g 来获得一个 a。请注意保留 HTML 标签。
\x -> f x (g x)

完成了。这里是Haskell预处理程序中实现的链接


6
考虑一下<*>的类型签名:
(<*>) :: Applicative f => f (a -> b) -> f a -> f b

与普通函数应用的类型签名 $ 相比较,可以进行比较:
($) :: (a -> b) -> a -> b

请注意它们非常相似!事实上,<*> 运算符有效地泛化了应用程序,以便可以根据所涉及的类型进行 重载。当使用最简单的 Applicative,即 Identity 时,这容易理解:

ghci> Identity (+) <*> Identity 1 <*> Identity 2
Identity 3

这种情况在稍微复杂一些的可应用函子中同样适用,例如Maybe

ghci> Just (+) <*> Just 1 <*> Just 2
Just 3
ghci> Just (+) <*> Nothing <*> Just 2
Nothing

对于(->) rApplicative实例执行一种函数组合,生成一个新函数,接受一种“上下文”并将其传递到所有值中以产生函数及其参数:
ghci> ((\_ -> (+)) <*> (+ 3) <*> (* 100)) 5
508

在上面的例子中,我只使用了<*>,因此我已经明确地将第一个参数写成忽略其参数并始终生成(+)。然而,Applicative类型类还包括pure函数,它具有相同的目的,即将纯值“提升”到应用函子中:
ghci> (pure (+) <*> (+ 3) <*> (* 100)) 5
508

实际上,你很少会看到 pure x <*> y 的用法,因为它与 x <$> y 在 Applicative 法则下完全等价,因为 <$> 只是 fmap 的中缀同义词。因此,我们有以下常见用法:
ghci> ((+) <$> (+ 3) <*> (* 100)) 5
508

更普遍地说,如果你看到任何像这样的表达式:
f <$> a <*> b

……你可以把它看作是普通函数应用程序f a b的变体,只不过是在特定可应用实例的语言环境下。事实上,一个最初的可应用方案提出了“习语括号”的概念,这将为以上表达式添加以下语法糖:

(| f a b |)

然而,Haskell程序员似乎对中缀运算符很满意,因此添加额外语法的收益被认为不值一提。因此,<$><*>仍然是必需的。


当我尝试在ghci(7.10.3和8.0.1)中运行Maybe (+) <*> Maybe 1 <*> Maybe 2时,我得到了Data constructor not in scope: Maybe ...的错误提示。我错过了什么吗? - Lee Duhem
顺便说一句,Just (+) <*> Just 1 <*> Just 2 得到的结果是 Just 3 - Lee Duhem
@LeeDuhem 不好意思,我的错,我不知道当时写那个的时候在想什么。谢谢你指出来! - Alexis King
非常感谢您的帮助性答案。 - Lee Duhem

4
让我们来看一下这些函数的类型(以及我们自动获得的定义):
(<$>) :: (a -> b) -> (r -> a) -> r -> b
f <$> g = \x -> f (g x)

(<*>) :: (r -> a -> b) -> (r -> a) -> r -> b
f <*> g = \x -> f x (g x)

在第一种情况下,<$> 实际上只是函数组合。更简单的定义是 (<$>) = (.)
第二种情况有些令人困惑。我们的第一个输入是一个函数 f :: r -> a -> b,我们需要得到一个类型为 b 的输出。我们可以将 x :: r 作为第一个参数提供给 f,但我们唯一能够得到一个类型为 a 的东西作为第二个参数的方式是将 g :: r -> a 应用到 x :: r 上。
有趣的是,<*> 实际上是来自于 SKI 组合演算 的函数 S,而 (-> r)pure 则是常数函数 K :: a -> b -> a

1
有趣的是,我们的思维火车几乎完全一致。 :D - ThreeFx

4
作为一名Haskell新手,我会尽力解释最好的方法。 <$>操作符与将函数映射到另一个函数相同。
当你这样做时:
(+) <$> (+3)

你基本上正在执行以下操作:
fmap (+) (+3)

上述代码将调用 (->) r 的 Functor 实现,其实现如下所示:
fmap f g = (\x -> f (g x))

因此,fmap (+) (+3)的结果是(\x -> (+) (x + 3))

请注意,此表达式的结果类型为a -> (a -> a)

这是一个应用函子!这就是为什么您可以将 (+) <$> (+3)的结果传递给<*>运算符的原因!

你可能会问为什么它是一个应用函子?让我们看一下<*>的定义:

f (a -> b) -> f a -> f b 

注意第一个参数与我们返回的函数定义相匹配。
现在,如果我们看一下<*>运算符的实现,它看起来像这样:
f <*> g = (\x -> f x (g x))

所以,当我们把所有这些部分组合在一起时,就得到了这个:
(+) <$> (+3) <*> (+5)
(\x -> (+) (x + 3)) <*> (+5)
(\y -> (\x -> (+) (x + 3)) y (y + 5))
(\y -> (+) (y + 3) (y + 5))

这不是\x -> f x g x,而是\x -> f x (g x) - user2407038

3
< p > (->) e FunctorApplicative实例往往会有点令人困惑。将(->) e视为Reader e的"未穿衣"版本可能会有所帮助。

newtype Reader e a = Reader
  { runReader :: e -> a }

名称e意味着"环境"。类型Reader e a应该被理解为"一种计算,根据类型为e的环境生成一个类型为a的值"。

给定类型为Reader e a的计算,您可以修改其输出:

instance Functor (Reader e) where
  fmap f r = Reader $ \e -> f (runReader r e)

也就是说,首先在给定的环境中运行计算,然后应用映射函数。
instance Applicative (Reader e) where
  -- Produce a value without using the environment
  pure a = Reader $ \ _e -> a

  -- Produce a function and a value using the same environment;
  -- apply the function to the value

  rf <*> rx = Reader $ \e -> (runReader rf e) (runReader rx e)

你可以像对待其他的应用函子一样,使用通常的“Applicative”推理方式。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接