英文:
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, <*>
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
,但是,<*>
是函数应用的泛化形式,其中参数函数、参数值和结果值都包含在 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] -> [Int]
Inc [] = []
Inc (n:ns) = n + 1 : inc ns
or
sqr :: [Int] -> [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->b) -> [a] -> [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 => f Int -> 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 -> f a
fmap1 :: (a -> b) -> f a -> f b
fmap2 :: (a -> b -> c) -> f a -> f b -> 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-> f a
(<*>) :: f (a -> b) -> f a -> 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, <*>
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 1
是 Maybe (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
的角色。通过进一步使用 (<*>)
,可以将此通用化为 fmap3
、fmap4
等等:
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 -> Int -> 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 -> (Int -> Int)
That is, (+)
takes an Int
and produces an Int -> 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 -> Int -> Int)
As before, that can be read as:
pure (+) :: Maybe (Int -> (Int -> Int))
Since the function type above is actually that of a one-argument function, we can use it with (<*>)
just fine:
(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
pure (+) :: Maybe (Int -> Int -> Int)
Just 1 :: Maybe Int
pure (+) <*> Just 1 :: Maybe (Int -> Int)
So pure (+) <*> Just 1
is Maybe (Int -> Int)
, and we can use it with (<*>)
and another 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
(Note that the <*>
operator associates to the left, so pure (+) <*> Just 1 <*> Just 2
amounts to (pure (+) <*> Just 1) <*> Just 2
.)
So (<*>)
can play the role of fmap2
. This can be generalised to fmap3
, fmap4
and so forth by using (<*>)
further times:
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
Since, by the class laws, pure f <*> u = fmap f u
, you'll usually see this kind of thing written in a slightly different style using (<$>)
, which is fmap
as an infix operator:
ghci> (1 +) <$> Just 1
Just 2
ghci> (*) <$> Just 2 <*> Just 3
Just 6
ghci> threeArgs <$> Just 2 <*> Just 3 <*> Just 4
Just 24
On a final note, liftA2
(which is how fmap2
is actually called in the base library) can be defined using (<*>)
, as we have already noted:
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f u v = f <$> u <*> v
It is also possible to turn things around and, if we already have liftA2
, define (<*>)
using it. That being so, they are ultimately equivalent:
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
u <*> v = liftA2 id u v
The idea here is that if u
already holds a -> b
functions, those functions can be applied as they are to the a
values from v
. That being so, liftA2
is given id :: a -> a
, the do-nothing function, which in this case will be specialised to the type (a -> b) -> (a -> b)
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论