如何将(<*>)推广到多个参数?

huangapple go评论59阅读模式
英文:

How does (<*>) generalise fmap to multiple arguments?

问题

I'm currently reading through some chapters regarding applicative and effectful programming.

这是我目前正在阅读的一些关于应用程序和有副作用编程的章节。

The chapters begins with functors which I understand as instead of mapping a specific data structure to a specific datatype it allows you to map any data structure to a data type e.g.:

这些章节以我理解的函子开始,函子不是将特定数据结构映射到特定数据类型,而是允许你将任何数据结构映射到数据类型,例如:

Both of these functions above can be abstracted by using map function which can be defined as:

上面的这两个函数都可以使用 map 函数进行抽象化,map 函数可以定义如下:

which in turn allows you to do inc = map (+1) or sqr = map(^2).

这反过来允许你执行 inc = map (+1)sqr = map(^2)

However, the above map functions only allows you to map list to list, however if we want to do something similar but if we want it to allow different data structures like trees or maybe, that's where functors come in e.g. if you want to add any type of data structures

然而,上面的 map 函数只允许你将列表映射到列表,但是如果我们想要做类似的事情,但是希望允许不同的数据结构,比如树,或者其他类型,这就是函子的用武之地,例如,如果你想要添加任何类型的数据结构

Where I get confused now is in the book it says instead of limiting to a single argument in functors we can make it such that it accepts multiple arguments. E.g.:

我现在感到困惑的地方是在书中它说,与其在函子中限制为单个参数,我们可以使其接受多个参数。例如:

And if we do fmap2 (+) (Just 1) (Just 2) this will return Just 3, which makes sense as explained above.

如果我们执行 fmap2 (+) (Just 1) (Just 2),这将返回 Just 3,如上面所解释的那样,这是有道理的。

However the book now starts to talk about using currying to allow us to get multiple arguments:

然而,现在书中开始讨论使用柯里化来允许我们获得多个参数:

I don't see the difference between this and the functions above.

我看不出这与上面的函数有什么不同。

I kinda understand the book saying that pure converts an element to be the data structure f. However, &lt;*&gt; being the generalised form of function application for which the argument function, the argument value, and the result value are all contained in f structures is the part I don't understand.

我有点理解书中说 pure 将一个元素转换为数据结构 f,但是,&lt;*&gt; 是函数应用的泛化形式,其中参数函数、参数值和结果值都包含在 f 结构中,这部分我不太理解。

I've seen other posts asking what are applicative effects, but I still didn't quite understand it.

我看过其他帖子询问什么是应用效应,但我仍然没有完全理解它。

英文:

I'm currently reading through some chapters regarding applicative and effectful programming.

The chapters begins with functors which I understand as instead of mapping a specific data structure to a specific datatype it allows you to map any data structure to a data type e.g.:

Inc :: [Int] -&gt; [Int]
Inc [] = []
Inc (n:ns) = n + 1 : inc ns

or

sqr :: [Int] -&gt; [Int]
sqr [] = []
sqr (n:ns) = n ^ 2 : sqr ns

Both of these functions above can be abstracted by using map function which can be defined as:

map :: (a-&gt;b) -&gt; [a] -&gt; [b]
map f [] = []
map f (x:xs) = fx : map f xs

which in turn allows you to do inc = map (+1) or sqr = map(^2).

However, the above map functions only allows you to map list to list, however if we want to do something similar but if we want it to allow different data structures like trees or maybe, that's where functors come in e.g. if you want to add any type of data structures

inc :: Functor f =&gt; f Int -&gt; f Int
inc = fmap (+1)

Where I get confused now is in the book it says instead of limiting to a single argument in functors we can make it such that it accepts multiple arguments. E.g.:

fmap0 :: a -&gt; f a
fmap1 :: (a -&gt; b) -&gt; f a -&gt; f b
fmap2 :: (a -&gt; b -&gt; c) -&gt; f a -&gt; f b -&gt; f c

And if we do fmap2 (+) (Just 1) (Just 2) this will return Just 3, which makes sense as explained above.

However the book now starts to talk about using currying to allow us to get multiple arguments:

pure :: a-&gt; f a
(&lt;*&gt;) :: f (a -&gt; b) -&gt; f a -&gt; f b 

I don't see the difference between this and the functions above.

I kinda understand the book saying that pure converts an element to be the data structure f. However, &lt;*&gt; being the generalised form of function application for which the argument function,
the argument value, and the result value are all contained in f structures is the part I don't understand.

I've seen other posts asking what are applicative effects, but I still didn't quite understand it.

答案1

得分: 5

让我们假设你想在两个Maybe Int值上使用(+)(+)的类型(专门化为Int)是:

(+) :: Int -> Int -> Int

虽然我们通常认为(+)是一个接受两个参数的函数,但是所有的 Haskell 函数实际上只接受一个参数。在实际使用中,这意味着我们可以添加一对多余的括号以强调,并将上面的类型解释为:

(+) :: Int -> (Int -> Int)

也就是说,(+)接受一个Int并产生一个Int -> Int函数。例如,(+) 1(或等效地,(1 +),使用运算符部分语法)是一个将1添加到其参数的函数。

现在,Maybe 函子的pure (+)的类型是:

pure (+) :: Maybe (Int -> Int -> Int)

与之前一样,这可以解释为:

pure (+) :: Maybe (Int -> (Int -> Int))

由于上面的函数类型实际上是一个单参数函数的类型,我们可以很好地将其与 (<*>) 一起使用:

(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
pure (+) :: Maybe (Int -> Int -> Int)
Just 1 :: Maybe Int
pure (+) <*> Just 1 :: Maybe (Int -> Int)

所以 pure (+) <*> Just 1Maybe (Int -> Int),我们可以将其与 (<*>) 和另一个 Maybe Int 一起使用:

(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
pure (+) <*> Just 1 :: Maybe (Int -> Int)
Just 2 :: Maybe Int
pure (+) <*> Just 1 <*> Just 2 :: Maybe Int

(请注意,(<*>) 运算符从左到右关联,因此 pure (+) <*> Just 1 <*> Just 2 相当于 (pure (+) <*> Just 1) <*> Just 2。)

因此,(<*>) 可以扮演 fmap2 的角色。通过进一步使用 (<*>),可以将此通用化为 fmap3fmap4 等等:

ghci> :{
ghci| threeArgs :: Int -> Int -> Int -> Int
ghci| threeArgs x y z = x * y * z
ghci| :}
ghci> pure threeArgs <*> Just 2 <*> Just 3 <*> Just 4
Just 24
ghci> pure threeArgs <*> Just 2 <*> Nothing <*> Just 4
Nothing

由于根据类法则,pure f <*> u = fmap f u,通常会看到以稍微不同的方式编写此类内容,使用 (<$>),它是 fmap 的中缀运算符:

ghci> (1 +) <$> Just 1
Just 2
ghci> (*) <$> Just 2 <*> Just 3
Just 6
ghci> threeArgs <$> Just 2 <*> Just 3 <*> Just 4
Just 24

最后,liftA2(实际上是基础库中使用的 fmap2)可以使用 (<*>) 定义,如我们之前提到的:

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f u v = f <$> u <*> v

也可以反过来,如果我们已经有了 liftA2,可以使用它来定义 (<*>)。因此,它们最终是等价的:

(<*>) :: Applicative f => f (a -> b) -> f a -> f b
u <*> v = liftA2 id u v

这里的想法是,如果 u 已经包含 a -> b 函数,那么这些函数可以直接应用于 v 中的 a 值。因此,liftA2 被给定了 id :: a -> a,这是一个什么都不做的函数,在这种情况下将被专门化为类型 (a -> b) -> (a -> b)

英文:

Let's say you want to use (+) on two Maybe Int values. The type of (+) (specialised to Int) is:

(+) :: Int -&gt; Int -&gt; Int

While we usually think of (+) as a two-argument function, all Haskell functions take just one argument. In practice, that means we can add a pair of redundant parentheses for emphasis, and read the type above as:

(+) :: Int -&gt; (Int -&gt; Int)

That is, (+) takes an Int and produces an Int -&gt; Int function. For instance, (+) 1 (or, equivalently, (1 +), using operator section syntax) is a function that adds 1 to its argument.

Now, the type of pure (+) for the Maybe functor is:

pure (+) :: Maybe (Int -&gt; Int -&gt; Int)

As before, that can be read as:

pure (+) :: Maybe (Int -&gt; (Int -&gt; Int))

Since the function type above is actually that of a one-argument function, we can use it with (&lt;*&gt;) just fine:

        (&lt;*&gt;)       :: Maybe (a   -&gt; b)          -&gt; Maybe a   -&gt; Maybe b
pure (+)            :: Maybe (Int -&gt; Int -&gt; Int)
             Just 1 ::                              Maybe Int
pure (+) &lt;*&gt; Just 1 ::                                           Maybe (Int -&gt; Int)

So pure (+) &lt;*&gt; Just 1 is Maybe (Int -&gt; Int), and we can use it with (&lt;*&gt;) and another Maybe Int:

                   (&lt;*&gt;)       :: Maybe (a   -&gt; b)   -&gt; Maybe a   -&gt; Maybe b
pure (+) &lt;*&gt; Just 1            :: Maybe (Int -&gt; Int)
                        Just 2 ::                       Maybe Int
pure (+) &lt;*&gt; Just 1 &lt;*&gt; Just 2 ::                                    Maybe Int

(Note that the &lt;*&gt; operator associates to the left, so pure (+) &lt;*&gt; Just 1 &lt;*&gt; Just 2 amounts to (pure (+) &lt;*&gt; Just 1) &lt;*&gt; Just 2.)

So (&lt;*&gt;) can play the role of fmap2. This can be generalised to fmap3, fmap4 and so forth by using (&lt;*&gt;) further times:

ghci&gt; :{
ghci| threeArgs :: Int -&gt; Int -&gt; Int -&gt; Int
ghci| threeArgs x y z = x * y * z
ghci| :}
ghci&gt; pure threeArgs &lt;*&gt; Just 2 &lt;*&gt; Just 3 &lt;*&gt; Just 4
Just 24
ghci&gt; pure threeArgs &lt;*&gt; Just 2 &lt;*&gt; Nothing &lt;*&gt; Just 4
Nothing

Since, by the class laws, pure f &lt;*&gt; u = fmap f u, you'll usually see this kind of thing written in a slightly different style using (&lt;$&gt;), which is fmap as an infix operator:

ghci&gt; (1 +) &lt;$&gt; Just 1
Just 2
ghci&gt; (*) &lt;$&gt; Just 2 &lt;*&gt; Just 3
Just 6
ghci&gt; threeArgs &lt;$&gt; Just 2 &lt;*&gt; Just 3 &lt;*&gt; Just 4
Just 24

On a final note, liftA2 (which is how fmap2 is actually called in the base library) can be defined using (&lt;*&gt;), as we have already noted:

liftA2 :: Applicative f =&gt; (a -&gt; b -&gt; c) -&gt; f a -&gt; f b -&gt; f c
liftA2 f u v = f &lt;$&gt; u &lt;*&gt; v

It is also possible to turn things around and, if we already have liftA2, define (&lt;*&gt;) using it. That being so, they are ultimately equivalent:

(&lt;*&gt;) :: Applicative f =&gt; f (a -&gt; b) -&gt; f a -&gt; f b
u &lt;*&gt; v = liftA2 id u v

The idea here is that if u already holds a -&gt; b functions, those functions can be applied as they are to the a values from v. That being so, liftA2 is given id :: a -&gt; a, the do-nothing function, which in this case will be specialised to the type (a -&gt; b) -&gt; (a -&gt; b).

huangapple
  • 本文由 发表于 2023年5月21日 18:23:32
  • 转载请务必保留本文链接:https://go.coder-hub.com/76299401.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定