Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference between function composition operator (.) and fmap (<$>)

Currently reading through this article (which is pretty brilliant btw) and have a pretty simple question:

If I combine two functions like (+3) and (+2) with <$>, it seems to give me a new function that adds 5 to whatever is passed to it. If I do the same with the function composition operator, i.e. (+3) . (+2), would it not do the same thing? If that is true, is there a relationship here between these two operators such that they do the same thing in this simple case?

Is this even an intelligent question?

like image 984
Brendan Avatar asked Jan 11 '15 02:01

Brendan


2 Answers

The functions fmap and <$> both have the same type:

> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b
> :t (<$>)
(<$>) :: Functor f => (a -> b) -> f a -> f b

While the function . is

> :t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c

So how is it possible that we can use fmap on a function and end up with .? I'm assuming you understand what a Functor is, so now you have to understand that "functions" are Functors. How so?

> :i (->)
data (->) a b   -- Defined in `GHC.Prim'
instance Monad ((->) r) -- Defined in `GHC.Base'
instance Functor ((->) r) -- Defined in `GHC.Base'
instance Applicative ((->) a) -- Defined in `Control.Applicative'

Unlike Just, [] and Left, functions do not have a constructor that can be used. The Functor instance is applied to the syntax itself. We can see from :info in ghci that the syntactic arrow -> actually has an instance for functor.

What happens when we look at the type of +3?

> :t (+3)
(+3) :: Num a => a -> a

So the function (+3) is a Functor that accepts an a and returns an a. When we use fmap on a Functor and that also gives us back a Functor, we get nested Functors:

> :t fmap Just (Just 3)
fmap Just (Just 3) :: Num a => Maybe (Maybe a)
> :t fmap (replicate 5) [1,2,3]
fmap (replicate 5) [1,2,3] :: Num a => [[a]]

Likewise, when we apply fmap to two functions we get a function inside a function. The only difference is that they are fused together:

> :t (fmap (+3) (+2))
(fmap (+3) (+2)) :: Num a => a -> a

Why doesn't this result in the type (->) (->) a a? We have to remember that the first argument of fmap is a function (a -> b) and not necessarily a Functor. So when we do fmap g (Just 5) we can have any transformation. But whenever we perform fmap on a function we know that it will always result with a function inside of a function.

Thus fmap (+3) (+2) evaluates to something like this: \x -> (\x' -> x' + 3) (x + 2). That is a really roundabout way of writing (+3) . (+2).

> :t (fmap (+3) (+2))
(fmap (+3) (+2)) :: Num a => a -> a
> :t ((.) (+3) (+2))
((.) (+3) (+2)) :: Num a => a -> a

Normally to get around the concat problem (Maybe (Maybe a)) or [[a]] we actually need to rely on it being a Monad a, so that we can use a bind >>=. But functions (->) are a special case because we know that every single time we use fmap on a function, it will always give us a function in side of a function. This cannot be said for any other Functor except ->. As such we can be sure to always concatenate fmap on functions.

Therefore any f <$> g == f . g

Edit: A quick side note, if you do this fmap (+) (+0) you end up with a function inside a function. In this case the monadic bind (>>=) is actually needed to concatenate the functions:

> :t fmap (+) (+0)
fmap (+) (+0) :: Num a => a -> a -> a
> :t (+0) >>= (+)
(+0) >>= (+) :: Num b => b -> b
> let bindfunc = (+0) >>= (+)
> bindfunc 5
10

Which is not entirely unlike the behaviour we get when we do [1,2] >>= replicate 5:

> [1,2] >>= replicate 5
[1,1,1,1,1,2,2,2,2,2]
like image 185
TheCriticalImperitive Avatar answered Nov 03 '22 20:11

TheCriticalImperitive


To find information about the Functor instance for functions, match up the types to find the relevant instance:

fmap :: (a -> b) -> f a -> f b

Then here a ~ Int, b ~ Int and f ~ (->) Int.

You can see all of the Functor instances that come with GHC here. (->) is just an infix type operator with two type parameters. We usually see it applied as Int -> Int, but this is equivalent to (->) Int Int. There is a Functor instance for the (partially applied) type (->) r (for any type r::*).

Looking at the ((->) r) instance for Functor, we see that fmap = (.), so there is no practical difference between (+3) . (+2) and fmap (+3) (+2) (same as (+3) <$> (+2).

like image 41
crockeea Avatar answered Nov 03 '22 21:11

crockeea