3

Brent Yorgey's excellent UPenn Haskell course presents:

fmap2 :: Functor f => (a -> b -> c) -> (f a -> f b -> f c)
fmap2 h fa fb = undefined

The types of h, fa and fb break down to:

h  :: a -> b -> c
fa :: f a
fb :: f b

It's not clear to me why h refers to the entire function (a -> b -> c).

Why couldn't h refer to a and fa refer to (b -> c)?

Do the parentheses in (a -> b -> c) make the difference?

EDIT

Consider leftaroundabout's comment:

For anyone just reading this without knowing the particular course referred to: fmap2 can not be defined with that signature.

It actually needs to be liftA2 :: Applicative a => (a->b->c) -> (f a->f b->f c)

Community
  • 1
  • 1
Kevin Meredith
  • 41,036
  • 63
  • 209
  • 384
  • 3
    For anyone just reading this without knowing the particular course referred to: `fmap2` can _not_ be defined with that signature. It actually needs to be `liftA2 :: Applicative a => (a->b->c) -> (f a->f b->f c)`. – leftaroundabout Dec 24 '14 at 13:20

1 Answers1

12

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.

Community
  • 1
  • 1
Christian Conkle
  • 5,932
  • 1
  • 28
  • 52