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