One way to tackle the type inference problem is to represent it as a problem of solving a system of equations involving type variables. Consider, for instance, how we intuitively assign a type to the following function definition in Haskell:
twice f x = f (f x)
We begin by observing that twice is a function of two arguments so, generically, we have twice :: a -> (b -> c) for some unknown types a, b and c. We then observe that the arguments f and x occur as a function application f x in the right hand side, so we have the further requirement that a, the type of f, must itself be of the form d -> e and the type of x must be compatible with d -> e, so b must be the same as d. Then, we see that the term (f x) itself appears as an argument to f so the output type e of f must be compatible with its input type d. Finally, since the output of twice is f (f x), we must have c compatible with e. We can represent this reasoning in terms of the following set of ``type equations''.
a | = | d -> e | (because f is a function) |
b | = | d | (because f is applied to x |
e | = | d | (because f is applied to (f x) |
c | = | e | (because output of twice is f (f x) |
If we ``solve'' these equations, we come to the conclusion that all four type variables b, c, d and e must in fact be equal to each other and a is of the form b -> b (or c -> c or ...). However, there is no further restriction on these variables, so we arrive at the conclusion that
twice :: (b -> b) -> b -> b
Here, the variable b is universally quantified. Notice that by leaving the interpretation of b ``open'', we arrived a ``most general'' type for twice.
If we have an expression involving twice, such as twice f 5, we can deduce that since 5 is of type Int, the type variable b in the type of twice must be uniformly assigned the value Int. Thus, in twice f 5, the (unknown) function f must have type Int -> Int and the overall type of the expression must be Int.