Brendan
Brendan

Reputation: 511

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?

Upvotes: 8

Views: 1146

Answers (2)

crockeea
crockeea

Reputation: 21811

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

Upvotes: 8

TheCriticalImperitive
TheCriticalImperitive

Reputation: 1457

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]

Upvotes: 8

Related Questions