Let's spell out the type part systematically. We'll start with the types of uncurry
and ($)
:
uncurry :: (a -> b -> c) -> (a, b) -> c
($) :: (a -> b) -> a -> b
Since the target expression has ($)
as the argument of uncurry
, let's line up their types to reflect this:
uncurry :: (a -> b -> c) -> (a, b) -> c
($) :: (a -> b) -> a -> b
The whole type of ($)
lines up with the first argument type of uncurry
, and the argument and result types of ($)
line up with those of uncurry
's first argument as shown. This is the correspondence:
uncurry's a <==> ($)'s a -> b
uncurry's b <==> ($)'s a
uncurry's c <==> ($)'s b
This is kinda confusing, because the a
and b
type variables in one type are not the same as in the other (just like the x
in plusTwo x = x + 2
is not the same as the x
in timesTwo x = x * 2
). But we can rewrite the types to help up reason about this. In simple Haskell type signatures like this, any time you see a type variable you can replace all of its occurrences with any other type get a valid type as well. If you pick fresh type variables (type variables that don't appear anywhere in the original), you get an equivalent type (one that can be converted back to the original); if you pick a non-fresh type you get a specialized version of the original that works with a narrower range of types.
But anyway, let's apply this to the type of uncurry
::
-- Substitute a ==> x, b ==> y, c ==> z:
uncurry :: (x -> y -> z) -> (x, y) -> z
Let's redo the "line up" using the rewritten type:
uncurry :: (x -> y -> z) -> (x, y) -> z
($) :: (a -> b) -> a -> b
Now it's obvious: x <==> a -> b
, y <==> a
and z <==> b
. Now, substituting uncurry
's type variables for their counterpart types in ($)
, we get:
uncurry :: ((a -> b) -> a -> b) -> (a -> b, a) -> b
($) :: (a -> b) -> a -> b
And finally:
uncurry ($) :: (a -> b, a) -> b
So that's how you figure out the type. How about what it does? Well, the best way to do that in this case is to look at the type and think about it carefully, figuring out what we'd have to write to get a function of that type. Let's rewrite it this way to make it more mysterious:
mystery :: (a -> b, a) -> b
mystery = ...
Since we know mystery
is a function of one argument, we can expand this definition to reflect that:
mystery x = ...
We also know that its argument is a pair, so we can expand a bit more:
mystery (x, y) = ...
Since we know that x
is a function and y :: a
, I like to use f
to mean "function" and to name variables the same as their type—it helps me reason about the functions, so let's do that:
mystery (f, a) = ...
Now, what do we put in the right hand side? We know it must be of type b
, but we don't know what type b
is (it's actually whatever the caller chooses, so we can't know). So we must somehow make a b
using our function f :: a -> b
and value a :: a
. Aha! We can just call the function with the value:
mystery (f, a) = f a
We wrote this function without looking at uncurry ($)
, but it turns out that it does the same thing as uncurry ($)
does, and we can prove it. Let's start with the definitions of uncurry
and ($)
:
uncurry f (a, b) = f a b
f $ a = f a
Now, substituting equals for equals:
uncurry ($) (f, a) = ($) f a -- definition of uncurry, left to right
= f $ a -- Haskell syntax rule
= f a -- definition of ($), left to right
= mystery (f, a) -- definition of mystery, right to left
So one way to attack a type that you don't understand in Haskell is to just try and write some code that has that type. Haskell is different from other languages in that very often this is a better strategy than trying to read the code.