I want to derive the type of the following contrived applicator applied to the identity function. To achieve this I probably have to unify the type portion of the first argument (a -> [b])
with the type of id
:
ap :: (a -> [b]) -> a -> [b]
id :: a -> a
a -> [b]
a0 -> a0 -- fresh type vars
a ~ a0 -- mappings
[b] ~ a0
a0 -> a0 -- substitution
This is obviously wrong, since the expected type is [b] -> [b]
. There is an ambiguity within the unification, because a0
cannot be equivalent to both a
and [b]
, except for a ~ [b]
. But what is the rule that tells me to substitute a
with [b]
and not the other way around, as I would have to do with ap :: ([a] -> b) -> [a] -> b
for example.
I know this is a very specific question, sorry. Hopefully it is not too confusing!
Ok, new answer because I now understand the question being asked! To restate the question:
Given
Explain how the type of the expression
ap id
is derived.Answer:
Rename variables:
Unify:
Apply generativity a couple times, pulling the arguments from the
(->)
type constructor:Apply commutativity/transitivity of type equality:
Substitute the most specific types known into the types of
ap
andid
[b]
is the most specific type known, because it provides some restriction. The value must be a list of something. The other two equivalent type expressions just mean any type at all. You can think of unification as a constraint-solving process. You find the maximal type that satisfies the constraints provided, which amount to "it's a list of something" for this case.Now that the types are unified, the type of the function application is the type of the function's result:
I can see why the choice of
[b]
looks a little odd in this case, because only one of the three type expressions contributed factors to unify. There are more involved cases where constraints come from multiple places, though.Let's consider a more advanced case. This might involve some things you haven't seen before. If it does, I apologize for jumping straight to the deep end.
Given:
Unify the types of
f1
andf2
.Let's be really careful with this one. First up, rewrite all the types in terms of prefix application. Even the
(->)
types. This is going to be ugly:Unify and apply generativity twice to top-level
(->)
type constructors:And, just keep unifying and applying generativity:
Ok, we've built up a giant stack of constraints now. Choosing between
a
andc
orb
andd
doesn't matter, as they're equivalently constrained. Let's choose letters closer to the beginning of the alphabet when it doesn't matter.(->)
is more constrained thanp
, so it wins there, and(,) e
is more constrained thanf
. Call it a winner too.Then switch back to infix type constructors to make it pretty, and the unified type is:
Notice how each of the two starting types contributed a constraint to the final unified type.
f1
requires thep
type inf2
to be more specific, andf2
required thef
type inf1
to be more specific.Overall, this is a super-mechanical process. It's also fiddly and requires precise tracking of what you know. There's a reason we mostly leave it to the compiler to handle this. It is absolutely useful in the cases when something goes wrong and you want to double-check the process yourself to see why the compiler is reporting an error, though.