3

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.

duplode
  • 33,731
  • 7
  • 79
  • 150
P47HF1ND3R
  • 45
  • 7
  • For the <*> im also slightly confused as for instance f(a->b) doesn't that still means you're still taking in only and return b where both are in datastructure f ? so what if i have more arguments, surely i'll still have to defined more things in the f() like a->b->c and etc – P47HF1ND3R May 21 '23 at 10:26
  • I'm afraid I don't find your question very clear about what specifically it is you don't understand. Basically, `fmap` only allows you to "lift" a one-argument function `a -> b` into one of type `f a -> f b`. Applicatives also allow you to lift 0-argument "functions" (that is, just values), with `pure :: a -> f a`, and functions of 2 arguments via `liftA2 :: (a -> b -> c) -> f a -> f b -> f x`, which is a variant of `(<*>)`, and which extends to functions of an arbitrary number of arguments via currying. – Robin Zigmond May 21 '23 at 10:40
  • @RobinZigmond what do you mean by lift ? also I thought <*> is to allow you just define lifta(a->b) ->fa -> fb and use it with multiple arguments without defining liftA2 – P47HF1ND3R May 21 '23 at 10:54
  • "lift" is an informal term, that basically means to apply a function that accepts "ordinary values" to values in the functor. And your second sentence makes it sound like you've basically got it. The value of `(<*>)` is indeed that it lets you go from having `fmap` to be able to define `liftA2`, then `liftA3` and all the rest of them, up to arbitrarily many arguments. – Robin Zigmond May 21 '23 at 11:41
  • @RobinZigmond So technically you can just have liftA where f(a->b) -> fa -> fb and you can just do pure 1 <*> 2 <*> 3 <*> etc and not having to define lifta2 a3 etc even though lifta only have f(a->b) ? – P47HF1ND3R May 21 '23 at 11:47
  • what is `pure 1 <*> 2` (etc) supposed to mean? `pure 1` has type `f Int` (or equivalent numeric type), which can't be made of type `f (a -> b)`, so this is a type error and meaningless. – Robin Zigmond May 21 '23 at 12:21
  • @P47HF1ND3R: It seems like what you’re missing is that in `f (a -> b)`, the type parameter `b` can be a function type. And `<$> … <*> … <*>` associates to the left, like function application does. So: `(\ x y z -> x + y + z) <$> Just 1 <*> Just 2 <*> Just 3` = `Just (\ y z -> 1 + y + z) <*> Just 2 <*> Just 3` = `Just (\ z -> 1 + 2 + z) <*> Just 3` = `Just (1 + 2 + 3)`. – Jon Purdy May 21 '23 at 18:25

1 Answers1

5

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

duplode
  • 33,731
  • 7
  • 79
  • 150