我正在进入Haskell,发现《学习Haskell》这本书最有帮助。我已经到了关于应用函子的部分。
我对书中出现的以下内容感到困惑:
(\x y z -> [x, y, z]) <$> (+3) <*> (*2) <*> (/2) $ 5
其产生输出:
[8.0,10.0,2.5]
首先,我已经证实了我对ghci中关于操作符优先级的怀疑,因此上面的语句相当于下面的丑陋语句:
(((\x y z -> [x,y,z]) <$> (+3)) <*> (*2) <*> (/2)) $ 5
因此,很明显,发生的第一件事是通过(<$>)
中缀运算符调用fmap
。
这是我目前最困惑的问题。fmap
的定义(这里显示为中缀(<$>)
)是:
(<$>) :: (Functor f) => (a -> b) -> f a -> f b
但在我正在努力解决的等式中,(\x y z -> [x, y, z])
有三个参数,而不仅仅是一个。那么如何满足(a -> b)
类型的第一个参数呢?
我认为这可能与部分应用程序/咖喱有关,但我无法弄清楚。如果你能解释一下我会很感激的。我希望我已经把这个问题表达得足够好了。
6条答案
按热度按时间7vhp5slm1#
简单的回答:* Haskell中没有带多个参数的函数!*
有两个候选者可以称为“二元函数”:一个函数,它接受一个(single!)tuple,以及--到目前为止在Haskell中很流行-- curried functions。它们只接受一个参数,但结果又是一个函数。
所以,要弄清楚例如。
fmap (+)
可以,我们写在GHCi中自己测试:
Prelude> type IntF = Int -> Int
Prelude> let(#)=(+)::Int -> IntF
Prelude>:t fmap(#)
fmap(#)::函数f => f Int -> f IntF
xghobddn2#
考虑一个类型为
其中
d
是任何其他类型。由于currying,这可以被认为是具有以下类型的函数也就是说,一个接受
a
并返回b -> c -> d
类型的函数。如果应用fmap
,它现在是用作
(<*>)
左侧参数的正确类型。rsaldnfx3#
因为你可以接受一个3参数的函数,只给它一个参数,这样就得到了一个2参数的函数。所以你最终会得到一个双参数函数列表。然后,您可以再应用一个参数,最终得到一个单参数函数列表,最后应用最后一个参数,最终得到一个普通数字列表。
顺便说一句,这就是Haskell使用curried函数的原因。它使得编写这样的结构变得容易,这些结构适用于任何数量的函数参数。:-)
wlzqhblo4#
我个人觉得函数的应用函子示例有点奇怪。我将通过这个例子引导你,试图直观地理解发生了什么:
这将
(+3)
应用于内部函数的第一个参数。其他两个外部参数被传递给内部函数,不作修改。让我们添加一个applicative:
这将像以前一样将
(+3)
应用于第一个参数。对于applicative,第一个外部参数(1
)被应用(*2)
,并作为内部函数的第二个参数传递。第二个外部参数作为第三个参数不加修改地传递给内部函数。猜猜当我们使用另一个应用程序时会发生什么:
3个应用程序将同一个参数作为3个参数传递给内部函数。
这不是理论上可靠的解释,但它可以给予一个关于函数的应用示例如何工作的直觉。
moiiocjp5#
后台
让我们从
<*>
和pure
作为Applicative
示例的函数的定义开始。对于pure
,它将接受任何垃圾值,并返回x
。对于<*>
,可以将其视为将x
应用于f
,从中获取一个新函数,然后将其应用于g x
的输出。现在,让我们来看看
<$>
的定义。它只是fmap
的一个中缀版本。回想一下,
fmap
有以下实现:证明
f <$> x
就是pure f <*> x
让我们从
pure f <*> x
开始。将pure f
替换为(\_ -> f)
。现在,让我们应用
<*>
的定义,即f <*> g = \q -> f q (g q)
。注意,我们可以将
(\_ -> f) q
简化为f
。函数接受我们给予它的任何值,并返回f
。这看起来就像我们对
fmap
的定义!而<$>
运算符就是中缀fmap
。让我们记住这一点:
f <$> g
就是pure f <*> g
。了解
(\x y z -> [x, y, z]) <$> (+3) <*> (*2) <*> (/2) $ 5
第一步是重写表达式的左侧,使用
<*>
而不是<$>
。使用我们在上一节中刚刚证明的内容:所以完整的表达就变成了
让我们使用
<*>
的定义来简化第一个运算符现在让我们用
(\_ -> x)
代替pure x
。注意,a
变成了用作_
的垃圾值,并被消耗以返回函数(\x y z -> [x, y, z])
。现在让我们回顾完整的表达式,并处理下一个
<*>
。再次,让我们应用<*>
的定义。最后,让我们对最后一个
<*>
重复最后一次。请注意,它是一个接受单个值的函数。我们将输入
5
。这就是我们如何得到最终答案。
jrcvhitl6#
对于你的问题,特别是“...需要三个参数,而不仅仅是一个......",这是
applicative functor
的主题,函子无关紧要。因此,仅仅看到fmap的定义并不能帮助理解这种混乱。在澄清你的困惑之前,请允许我再次介绍函子和应用函子。
函数是Functors
正如定义所指,每个人都知道,functor * 允许将函数应用于泛型类型内部的值,而不改变泛型类型的结构 *。这意味着函数也是函子,根据源代码可以表示为
(->) r
。它提示
function
可以被另一个函数作为functor
应用,无论需要多少参数。例如,(+1)
是一个函子,如果第一个参数是相同的类型,任何函数都可以应用于它(注意,每个函数在技术上只有一个输入和输出类型)。在这里,入侵跟随你的思想。任何应用于函子的函数(也是函数)总是保持相同的参数。这无疑是正确的,以下计算输出是合乎逻辑的:
更一般地,在函数的方式来表达一个函子,它将是:
传导是,在应用之后,
c->
的输出需要输入,而b
需要其他输入。函数是应用函数
由于函数是Haskell中的第一个类,因此函数可以应用于functor。在此之后,为了解决将一个持有另一个函子的函子应用于另一个函子的问题,应用函子应运而生。
要正确理解
<*>
,必须知道函数是函子。然后,请看<*>
的签名,它与fmap
略有不同;但是,这就是为什么争论可能会减少。让我们先看看
fmap
,Nb
指的是b中函数参数的数量,而函子f
总是接受1
参数。因此,a->b
和f b
都需要1+Nb
参数。然而,<*>
将两个函子合并为一个,这意味着原始函数f (a -> b)
需要1+1+Nb
参数,但返回值接收1+Nb
参数。因此,当应用(<*>)时,输出中的参数减1。它丢失了,因为应用函子被合并了。这解释了为什么参数减少(从三个减少到一个)。让我们更进一步。参数数如何减少?只有
currying
才能做到。上面的解释说明了为什么当<*>
被调用时,如果我们从左到右说签名,参数会减少。现在,将
f b
折叠到f (a->b) -> f a
,并将输入传递给f b
,那么(f b) input
将是f (input -> b) -> f input
。由于f (a -> b )
总是可以折叠出来,最后,随着折叠的进行,完整的参数一个接一个地进行curry。因此,您的问题
(\x y z -> [x, y, z]) <$> (+3) <*> (*2) <*> (/2) $ 5
将以如下方式执行: