Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.:

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.

like image 652
P47HF1ND3R Avatar asked Oct 17 '25 01:10

P47HF1ND3R


1 Answers

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).

like image 76
duplode Avatar answered Oct 18 '25 20:10

duplode



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!