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.
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)
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With