Yes, the parentheses make the difference exactly as you say. Because (->)
is right-associative, but not mathematically associative, a parenthesized expression on the left side of a function arrow cannot be split up the way you're suggesting:
(a -> b) -> (f a -> f b) /= a -> b -> f a -> f b
The ->
operator is in this regard just like the exponentiation operator, ^
, which is notationally right-associative but not mathematically associative:
(2 ^ 2) ^ (2 ^ 2) /= 2 ^ 2 ^ 2 ^ 2
4 ^ 4 /= 2 ^ (2 ^ (2 ^ 2))
256 /= 2 ^ (2 ^ 4)
256 /= 2 ^ 16
256 /= 65536
(The analogy to exponentiation isn't my own invention; function types are "exponential types" in the same sense that (a, b)
is a "product type" and Either a b
is a "sum type." But note that a -> b
is analogous to b ^ a
, not a ^ b
. See this blog post for an example-heavy explanation; also this answer gives a mathematical overview of type algebra.)
The apparent oddity with fmap2
is that the type looks like it takes one parameter, but the definition looks like it takes three. Contrast this version, which to me at least looks more like the type signature:
fmap2 :: Functor f => (a -> b -> c) -> (f a -> f b -> f c)
fmap2 h = \fa fb -> undefined
Now we have a nice "one-argument" thing, fmap2 h = ...
, with a "two-argument" lambda on the right. The trick is that in Haskell these two expressions are equivalent[*]: the Haskell Report says the "function" form, with the parameters on the LHS, is "semantically equivalent" to a simple pattern binding of a lambda.
You could also rewrite the type to eliminate the parentheses on the right side of an arrow, again because ->
is right-associative:
(a -> b -> c) -> (f a -> f b -> f c)
== (a -> b -> c) -> f a -> f b -> f c
just like
(2 ^ 2 ^ 2) ^ (2 ^ 2 ^ 2)
== (2 ^ 2 ^ 2) ^ 2 ^ 2 ^ 2
[*]: They're semantically equivalent, but when compiled with GHC their performance characteristics can and sometimes do differ. GHC's optimizer treats f x = ...
and f = \x -> ...
differently.