Keys idea in deriving type signatures:
- Make sure you get your operator and function precedences right before you start.
Notice that function application has the highest precedence and associates to the left, so:
- The first argument is the one that matters most - deal with it first, and
- In type signatures,
->
associates to the right.
- Once you have the type of the first argument, substitute that type everywhere it appears.
Let's work through your example.
Types
(.) :: (b -> c) -> (a -> b) -> a -> c
map :: (a -> b) -> [a] -> [b]
uncurry :: (a -> b -> c) -> (a, b) -> c
Give each function non-overlapping type names
Firstly, that's confusing because there are lots of a
s and they don't all mean the same thing, so I'm going to rename the types with new letters each time.
(.) :: (b -> c) -> (a -> b) -> a -> c
map :: (d -> e) -> [d] -> [e]
uncurry :: (f -> g -> h) -> (f, g) -> h
Bracket the types, associating to the right
(.) :: (b -> c) -> ((a -> b) -> a -> c)
map :: (d -> e) -> ([d] -> [e])
uncurry :: (f -> (g -> h)) -> ((f, g) -> h)
Match the type of the first argument with the whole of that argument's type
Now let's look at the expression (.) map uncurry
. As you've now realised, putting the operator .
in brackets converts it to a function following normal function rules, so
(.) map uncurry
means ((.) map) uncurry
, and the first types to unify are from (.)
and map
.
Now (.)
has first argument (b->c)
, so (b->c)
has to unify with the type of map
:
(.) :: ( b -> c ) -> ((a -> b) -> (a -> c))
map :: (d -> e) -> ([d] -> [e])
when we substitute b ~ (d->e)
and c ~ ([d]->[e])
in the type of (.)
we get:
(.) :: ((d->e) -> ([d]->[e])) -> ((a -> (d->e)) -> (a -> ([d]->[e])))
and so map
becomes the first argument of that, so it disappears from the type signature when we supply it, giving
(.) map :: ((a -> (d->e)) -> (a -> ([d]->[e])))
Check with an interpreter like ghci or hugs
Hugs> :t (.) map
(map .) :: (a -> b -> c) -> a -> [b] -> [c]
Yup - when we add brackets due to ->
being right associative, those are the same.
Move on to the next argument
OK, so now we have
(.) map :: ((a -> (d->e)) -> (a -> ([d]->[e])))
uncurry :: (f -> (g -> h)) -> ((f, g) -> h)
Now it's terribly tempting to match those two first arguments since they look the same, but of course we need to match the first argument of (.) map
with the whole type of uncurry
:
Match the type of the first argument with the whole of that argument's type
(.) map :: (( a -> ( d -> e)) -> (a -> ([d]->[e])))
uncurry :: (f->(g->h)) -> ((f,g) -> h)
giving a ~ (f->(g->h))
, d ~ (f,g)
and e ~ h
:
(.) map :: (((f->(g->h)) -> ((f,g)-> h)) -> ((f->(g->h)) -> ([(f,g)]->[h])))
and applying that to uncurry gives
(.) map uncurry :: ((f->(g->h)) -> ([(f,g)]->[h])))
Check with an interpreter
Hugs> :t (.) map uncurry
map . uncurry :: (a -> b -> c) -> [(a,b)] -> [c]
Great - we did it!
Dealing with operators
If we take the example length . map (.) $ repeat id ++ [] ++ []
, we're going to need the fixities of all those operators, but we can bracket the function applications first because they take precedence:
length . map (.) $ repeat id ++ [] ++ []
length . (map (.)) $ (repeat id) ++ [] ++ []
Put the operators in order of precedence
Look up the fixities of the operators using the :i
command of your interpreter, and put them in order, highest first:
infixr 9 .
infixr 5 ++
infixr 0 $
The highest precedence operator .
gets bracketed first:
(length . (map (.))) $ (repeat id) ++ [] ++ []
Then ++
, which associates to the right:
(length . (map (.))) $ ((repeat id) ++ ([] ++ []))
and there's only one use of $
so I've not bothered bracketing it.
If you like, you can convert operators like ++
to functions (++)
, but I'm not at all convinced that will help you, so I'd leave it be, just remembering that the first argument is to the left.
It'll usually help to start with the most nested brackets.
(.) map uncurry
is(.)
withmap
anduncurry
as arguments, and not(.) (map uncurry)
. – duplode