haskell 适用函子:为什么fmap可以接受一个有多个参数的函数?

vvppvyoh  于 9个月前  发布在  其他
关注(0)|答案(6)|浏览(79)

我正在进入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)类型的第一个参数呢?
我认为这可能与部分应用程序/咖喱有关,但我无法弄清楚。如果你能解释一下我会很感激的。我希望我已经把这个问题表达得足够好了。

7vhp5slm

7vhp5slm1#

简单的回答:* Haskell中没有带多个参数的函数!*
有两个候选者可以称为“二元函数”:一个函数,它接受一个(single!)tuple,以及--到目前为止在Haskell中很流行-- curried functions。它们只接受一个参数,但结果又是一个函数。
所以,要弄清楚例如。fmap (+)可以,我们写

type IntF = Int -> Int

-- (+) :: Int -> IntF
-- fmap :: ( a -> b  ) ->  f a -> f b
--  e.g.:: (Int->IntF) -> f Int->f IntF

在GHCi中自己测试:
Prelude> type IntF = Int -> Int
Prelude> let(#)=(+)::Int -> IntF
Prelude>:t fmap(#)
fmap(#)::函数f => f Int -> f IntF

xghobddn

xghobddn2#

考虑一个类型为

f :: a -> b -> c -> d

其中d是任何其他类型。由于currying,这可以被认为是具有以下类型的函数

f :: a -> (b -> c -> d)

也就是说,一个接受a并返回b -> c -> d类型的函数。如果应用fmap

-- the type of fmap, which is also :: (a -> r) -> (f a -> f r)
fmap :: Functor f => (a -> r) -> f a -> f r

-- the type of f
f :: a -> (b -> c -> d)

-- so, setting r = b -> c -> d
fmap f :: f a -> f (b -> c -> d)

它现在是用作(<*>)左侧参数的正确类型。

rsaldnfx

rsaldnfx3#

因为你可以接受一个3参数的函数,只给它一个参数,这样就得到了一个2参数的函数。所以你最终会得到一个双参数函数列表。然后,您可以再应用一个参数,最终得到一个单参数函数列表,最后应用最后一个参数,最终得到一个普通数字列表。
顺便说一句,这就是Haskell使用curried函数的原因。它使得编写这样的结构变得容易,这些结构适用于任何数量的函数参数。:-)

wlzqhblo

wlzqhblo4#

我个人觉得函数的应用函子示例有点奇怪。我将通过这个例子引导你,试图直观地理解发生了什么:

>>> :t (\x y z -> [x, y, z]) <$> (+3)
... :: Num a => a -> a -> a -> [a]
>>> ((\x y z -> [x, y, z]) <$> (+3)) 1 2 3
[4,2,3]

这将(+3)应用于内部函数的第一个参数。其他两个外部参数被传递给内部函数,不作修改。
让我们添加一个applicative:

>>> :t (\x y z -> [x, y, z]) <$> (+3) <*> (*2)
... :: Num a => a -> a -> [a]
>>> ((\x y z -> [x, y, z]) <$> (+3) <*> (*2)) 1 2
[4,2,2]

这将像以前一样将(+3)应用于第一个参数。对于applicative,第一个外部参数(1)被应用(*2),并作为内部函数的第二个参数传递。第二个外部参数作为第三个参数不加修改地传递给内部函数。
猜猜当我们使用另一个应用程序时会发生什么:

>>> :t (\x y z -> [x, y, z]) <$> (+3) <*> (*2) <*> (/2)
... :: Fractional a => a -> [a]
>>> (\x y z -> [x, y, z]) <$> (+3) <*> (*2) <*> (/2) $ 1
[4.0,2.0,0.5]

3个应用程序将同一个参数作为3个参数传递给内部函数。
这不是理论上可靠的解释,但它可以给予一个关于函数的应用示例如何工作的直觉。

moiiocjp

moiiocjp5#

后台

让我们从<*>pure作为Applicative示例的函数的定义开始。对于pure,它将接受任何垃圾值,并返回x。对于<*>,可以将其视为将x应用于f,从中获取一个新函数,然后将其应用于g x的输出。

instance Applicative ((->) r) where  
    pure x = (\_ -> x)  
    f <*> g = \x -> f x (g x)

现在,让我们来看看<$>的定义。它只是fmap的一个中缀版本。

(<$>) :: (Functor f) => (a -> b) -> f a -> f b  
f <$> x = fmap f x

回想一下,fmap有以下实现:

instance Functor ((->) r) where  
    fmap f g = (\x -> f (g x))

证明f <$> x就是pure f <*> x

让我们从pure f <*> x开始。将pure f替换为(\_ -> f)

pure f <*> x 
= (\_ -> f) <*> x

现在,让我们应用<*>的定义,即f <*> g = \q -> f q (g q)

(\_ -> f) <*> x
= \q -> (\_ -> f) q (x q)

注意,我们可以将(\_ -> f) q简化为f。函数接受我们给予它的任何值,并返回f

\q -> (\_ -> f) q (x q)
= \q -> f (x q)

这看起来就像我们对fmap的定义!而<$>运算符就是中缀fmap

\q -> f (x q)
= fmap f x
= f <$> x

让我们记住这一点:f <$> g就是pure f <*> g

了解(\x y z -> [x, y, z]) <$> (+3) <*> (*2) <*> (/2) $ 5

第一步是重写表达式的左侧,使用<*>而不是<$>。使用我们在上一节中刚刚证明的内容:

(\x y z -> [x, y, z]) <$> (+3)
= pure (\x y z -> [x, y, z]) <*> (+3)

所以完整的表达就变成了

pure (\x y z -> [x, y, z]) <*> (+3) <*> (*2) <*> (/2) $ 5

让我们使用<*>的定义来简化第一个运算符

pure (\x y z -> [x, y, z]) <*> (+3)
= \a -> f a (g a) --substitute f and g
= \a -> pure (\x y z -> [x, y, z]) a ((+3) a)

现在让我们用(\_ -> x)代替pure x。注意,a变成了用作_的垃圾值,并被消耗以返回函数(\x y z -> [x, y, z])

\a -> (\_-> (\x y z -> [x, y, z])) a ((+3) a)
= \a -> (\x y z -> [x, y, z]) ((+3) a)

现在让我们回顾完整的表达式,并处理下一个<*>。再次,让我们应用<*>的定义。

(\a -> (\x y z -> [x, y, z]) ((+3) a)) <*> (*2)
= \b -> (\a -> (\x y z -> [x, y, z]) ((+3) a)) b ((*2) b)

最后,让我们对最后一个<*>重复最后一次。

(\b -> (\a -> (\x y z -> [x, y, z]) ((+3) a)) b ((*2) b)) <*> (/2)
= \c -> (\b -> (\a -> (\x y z -> [x, y, z]) ((+3) a)) b ((*2) b)) c ((/2) c)

请注意,它是一个接受单个值的函数。我们将输入5

(\c -> (\b -> (\a -> (\x y z -> [x, y, z]) ((+3) a)) b ((*2) b)) c ((/2) c)) 5
(\5 -> (\b -> (\a -> (\x y z -> [x, y, z]) ((+3) a)) b ((*2) b)) 5 ((/2) 5))
       (\b -> (\a -> (\x y z -> [x, y, z]) ((+3) a)) b ((*2) b)) 5 (2.5   )
       (\5 -> (\a -> (\x y z -> [x, y, z]) ((+3) a)) 5 ((*2) 5))   (2.5   )
              (\a -> (\x y z -> [x, y, z]) ((+3) a)) 5 (10    )    (2.5   )
              (\5 -> (\x y z -> [x, y, z]) ((+3) 5))   (10    )    (2.5   )           
                     (\x y z -> [x, y, z]) (8     )    (10    )    (2.5   )         

(\x y z -> [x, y, z]) (8) (10) (2.5)                     
= [8, 10, 2.5]

这就是我们如何得到最终答案。

jrcvhitl

jrcvhitl6#

对于你的问题,特别是“...需要三个参数,而不仅仅是一个......",这是applicative functor的主题,函子无关紧要。因此,仅仅看到fmap的定义并不能帮助理解这种混乱。
在澄清你的困惑之前,请允许我再次介绍函子和应用函子。

函数是Functors

正如定义所指,每个人都知道,functor * 允许将函数应用于泛型类型内部的值,而不改变泛型类型的结构 *。这意味着函数也是函子,根据源代码可以表示为(->) r

instance Functor ((->) r) where
    fmap = (.)

它提示function可以被另一个函数作为functor应用,无论需要多少参数。例如,(+1)是一个函子,如果第一个参数是相同的类型,任何函数都可以应用于它(注意,每个函数在技术上只有一个输入和输出类型)。

ghci> f1 = fmap (+1) (*2)
ghci> :t f1
f1 :: Num b => b -> b
ghci> f2 = fmap (+) (*2)
ghci> :t f2
f2 :: Num a => a -> a -> a

在这里,入侵跟随你的思想。任何应用于函子的函数(也是函数)总是保持相同的参数。这无疑是正确的,以下计算输出是合乎逻辑的:

ghci> f1 2
5
ghci> f2 2 1
5

更一般地,在函数的方式来表达一个函子,它将是:

fmap :: Functor f => (a -> b) -> f a -> f b
fmap :: (a -> b) -> ((->) c a) -> ((->) c b)
fmap :: (a -> b) -> (c -> a) -> (c -> b)

传导是,在应用之后,c->的输出需要输入,而b需要其他输入。

函数是应用函数

由于函数是Haskell中的第一个类,因此函数可以应用于functor。在此之后,为了解决将一个持有另一个函子的函子应用于另一个函子的问题,应用函子应运而生。

class (Functor f) => Applicative f where  
    pure :: a -> f a  
    (<*>) :: f (a -> b) -> f a -> f b

要正确理解<*>,必须知道函数是函子。然后,请看<*>的签名,它与fmap略有不同;但是,这就是为什么争论可能会减少。

fmap :: Functor f => (a -> b) -> f a -> f b
class (Functor f) => Applicative f where  
    pure :: a -> f a  
    (<*>) :: f (a -> b) -> f a -> f b

让我们先看看fmapNb指的是b中函数参数的数量,而函子f总是接受1参数。因此,a->bf 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将以如下方式执行:

(\x y z -> [x, y, z]) <$> (+3) <*> (*2) <*> (/2) $ 5
= (\x y -> [x , y, 2.5]) <$> (+3) <*> (*2)$ 5
= (\x -> [x, 10, 2.5] <$> (+3) $ 5
= [8, 10, 2.5]

相关问题