Search code examples
haskelloperatorsfunctorfunction-composition

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?


Solution

  • 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]