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 twoMaybe Intvalues. The type of(+)(specialised toInt) is: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:That is,
(+)takes anIntand produces anInt -> Intfunction. For instance,(+) 1(or, equivalently,(1 +), using operator section syntax) is a function that adds1to its argument.Now, the type of
pure (+)for theMaybefunctor is:As before, that can be read as:
Since the function type above is actually that of a one-argument function, we can use it with
(<*>)just fine:So
pure (+) <*> Just 1isMaybe (Int -> Int), and we can use it with(<*>)and anotherMaybe Int:(Note that the
<*>operator associates to the left, sopure (+) <*> Just 1 <*> Just 2amounts to(pure (+) <*> Just 1) <*> Just 2.)So
(<*>)can play the role offmap2. This can be generalised tofmap3,fmap4and so forth by using(<*>)further times: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 isfmapas an infix operator:On a final note,
liftA2(which is howfmap2is actually called in the base library) can be defined using(<*>), as we have already noted:It is also possible to turn things around and, if we already have
liftA2, define(<*>)using it. That being so, they are ultimately equivalent:The idea here is that if
ualready holdsa -> bfunctions, those functions can be applied as they are to theavalues fromv. That being so,liftA2is givenid :: a -> a, the do-nothing function, which in this case will be specialised to the type(a -> b) -> (a -> b).