4

I am trying to understand how fmap fmap applies to a function like say (*3).

The type of fmap fmap:

(fmap fmap):: (Functor f1, Functor f) => f (a -> b) -> f (f1 a -> f1 b)

Type of (*3):

(*3) :: Num a => a -> a

Which means that the signature a -> a corresponds to f (a -> b), right?

Prelude> :t (fmap fmap (*3))
(fmap fmap (*3)):: (Num (a -> b), Functor f) => (a -> b) -> f a -> f b

I have tried creating a simple test:

test :: (Functor f) => f (a -> b) -> Bool 
test f = True

And feeding (*3) into it, but I get:

*Main> :t (test (*3))

<interactive>:1:8:
    No instance for (Num (a0 -> b0)) arising from a use of ‘*’
    In the first argument of ‘test’, namely ‘(* 3)’
    In the expression: (test (* 3))

Why is that happening?

egdmitry
  • 2,071
  • 15
  • 18
  • 3
    `Num a => a -> a` and `f (x -> y)` do not align well, you end up with `f ~ (->) a` and `a ~ x -> y`, hence `Num (x -> y)`. A more interesting one might be `fmap ($ [1, 2, 3]) $ fmap fmap $ fmap (+) $ Just 10`, which returns `Just [11,12,13]`. A more useful `Functor` combinator might be `fmap fmap fmap`, which lets you just list a function through two different `Functor`s: `(.:) = fmap fmap fmap`; `(10*) .: [Just 1, Nothing, Just 3] == [Just 10, Nothing, Just 30]` – bheklilr Mar 03 '15 at 06:59
  • Note that `fmap fmap fmap` is equivalent to `fmap . fmap` since the outer functor is forced to be `(->) (a -> b)` (which is why asking for the type of `fmap fmap fmap` only specifies two functors in its constraints). – Chris Taylor Mar 03 '15 at 07:51
  • But why does `(fmap fmap (*3))` typecheck? I guess I am just with ghci difference in handling two functions with the argument of the same type (`(fmap fmap (*3))` and `test (*3)`) – egdmitry Mar 03 '15 at 08:18
  • 1
    `fmap fmap fmap` looks really weird – Bartek Banachewicz Mar 03 '15 at 09:52
  • 1
    `test (*3)` produces a type error because the type variables `a` and `b` are not present in the output type. Since there is a `Num (a -> b)` constraint, this constraint must be resolved when this function is applied (because it cannot be resolved later). Since no such instance exists, you get a type error at that moment. Change it to `test :: (Functor f) => f (a -> b) -> (a,b)` and you will get `test (*3) :: Num (a -> b) => (a, b)`. – user2407038 Mar 03 '15 at 17:30

1 Answers1

5

Polymorphism is dangerous when you do not know what you are doing. Both fmap and (*) are polymorphic functions and using them blindly can lead to very confusing (and possibly incorrect) code. I have answered a similar question before:

What is happening when I compose * with + in Haskell?

In such cases, I believe that looking at the types of values can help you figure out where you are going wrong and how to rectify the problem. Let's start with the type signature of fmap:

fmap :: Functor f => (a -> b) -> f a -> f b
                     |______|    |________|
                         |            |
                      domain      codomain

The type signature of fmap is easy to understand. It lifts a function from a to b into the context of a functor, whatever that functor may be (e.g. list, maybe, either, etc.).

The words "domain" and "codomain" just mean "input" and "output" respectively. Anyway, let's see what happens when we apply fmap to fmap:

fmap :: Functor f => (a -> b) -> f a -> f b
                     |______|
                         |
                       fmap :: Functor g => (x -> y) -> g x -> g y
                                            |______|    |________|
                                                |            |
                                                a    ->      b

As you can see, a := x -> y and b := g x -> g y. In addition, the Functor g constraint is added. This gives us the type signature of fmap fmap:

fmap fmap :: (Functor f, Functor g) => f (x -> y) -> f (g x -> g y)

So, what does fmap fmap do? The first fmap has lifted the second fmap into the context of a functor f. Let's say that f is Maybe. Hence, on specializing:

fmap fmap :: Functor g => Maybe (x -> y) -> Maybe (g x -> g y)

Hence fmap fmap must be applied to a Maybe value with a function inside it. What fmap fmap does is that it lifts the function inside the Maybe value into the context of another functor g. Let's say that g is []. Hence, on specializing:

fmap fmap :: Maybe (x -> y) -> Maybe ([x] -> [y])

If we apply fmap fmap to Nothing then we get Nothing. However, if we apply it to Just (+1) then we get a function that increments every number of a list, wrapped in a Just constructor (i.e. we get Just (fmap (+1))).

However, fmap fmap is more general. What it actually does it that it looks inside a functor f (whatever f may be) and lifts the function(s) inside f into the context of another functor g.

So far so good. So what's the problem? The problem is when you apply fmap fmap to (*3). This is stupid and dangerous, like drinking and driving. Let me tell you why it's stupid and dangerous. Take a look at the type signature of (*3):

(*3) :: Num a => a -> a

When you apply fmap fmap to (*3) then the functor f is specialized to (->) r (i.e. a function). A function is a valid functor. The fmap function for (->) r is simply function composition. Hence, the type of fmap fmap on specializing is:

fmap fmap :: Functor g => (r -> x -> y) -> r -> g x -> g y

-- or

(.)  fmap :: Functor g => (r -> x -> y) -> r -> g x -> g y
                          |___________|
                                |
                              (*3) :: Num a => a ->    a
                                               |       |
                                               |    ------
                                               |    |    |
                                               r -> x -> y

Do you see why it's stupid and dangerous?

  1. It's stupid because you are applying a function which expects an input function with two arguments (r -> x -> y) to a function with only one argument, (*3) :: Num a => a -> a.
  2. It's dangerous because the output of (*3) is polymorphic. Hence, the compiler doesn't tell you that you are doing something stupid. Luckily, because the output is bounded you get a type constraint Num (x -> y) which should indicate that you have gone wrong somewhere.

Working out the types, r := a := x -> y. Hence, we get the following type signature:

fmap . (*3) :: (Num (x -> y), Functor g) => (x -> y) -> g x -> g y

Let me show you why it's wrong using values:

  fmap . (*3)
= \x -> fmap (x * 3)
             |_____|
                |
                +--> You are trying to lift a number into the context of a functor!

What you really want to do is apply fmap fmap to (*), which is a binary function:

(.) fmap :: Functor g => (r -> x -> y) -> r -> g x -> g y
                         |___________|
                               |
                              (*) :: Num a => a -> a -> a
                                              |    |    |
                                              r -> x -> y

Hence, r := x := y := a. This gives you the type signature:

fmap . (*) :: (Num a, Functor g) => a -> g a -> g a

This makes even more sense when you see the values:

  fmap . (*)
= \x -> fmap (x *)

Hence, fmap fmap (*) 3 is simply fmap (3*).

Finally, you have the same problem with your test function:

test :: Functor f => f (a -> b) -> Bool

On specializing functor f to (->) r we get:

test :: (r -> a -> b) -> Bool
        |___________|
              |
            (*3) :: Num x => x ->    x
                             |       |
                             |    ------
                             |    |    |
                             r -> a -> b

Hence, r := x := a -> b. Thus we get type signature:

test (*3) :: Num (a -> b) => Bool

Since neither a nor b appear in the output type, the constraint Num (a -> b) must be resolved immediately. If a or b appeared in the output type then they could be specialized and a different instance of Num (a -> b) could be chosen. However, because they don't appear in the output type the compiler has to decide which instance of Num (a -> b) to choose immediately; and because Num (a -> b) is a stupid constraint which doesn't have any instance, the compiler throws an error.

If you try test (*) then you won't get any error, for the same reason that I mentioned above.

Community
  • 1
  • 1
Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299