alpha -> (alpha -> beta) -> betaLet us try to construct an expression with this type. Clearly, it must be a (curried) function with two arguments, i.e. must have the following structure.
fn ... => fn ... => ...Let us now fill in the dots. Let us call x and f, respectively, the first and second argument of the function. Clearly x: alpha and f: alpha -> beta. Now we have only to construct an expression with type beta, and use it as result.
Let us use an analogy: think of alpha as milk, and of beta as yogurt. Then f represents a machine that transforms milk into yogurt. Now, we have some milk (x), we have the machine, and we want to obtain yougurt. What shall we do? The answer is, of course,
turn on the machine and put the milk in iti.e. use f with input x. Hence, the resulting expression is:
fn x => fn f => f x
(alpha * beta -> gamma) -> alpha -> beta -> gammaAn expression with this type must have the following structure:
fn f => fn x => fn y => ...where f: alpha * beta -> gamma, x: alpha, and x: beta. In order to fill in the result, let us use again an analogy similar to the above.
Look at f as a machine that, when provided with flour (alpha) and yeast (beta) together, it makes bread (gamma). We have some flour (x), we have some yeast (y), how to obtain bread? The answer is, of course,
use f and give it in input x and y together.Hence the solution is:
fn f => fn x => fn y => f(x,y)
fn f => f fLet us see why it does not have a type. If it did, it should be something like
(alpha -> beta) -> betawhere f: alpha -> beta and (f f): beta. However, since f is also the argument in (f f), we should have f: alpha. But the equation alpha = alpha -> beta is unsatisfiable (at least for finite types, as it is the case in the Type Theory of ML. Such equation admits only an infinite solution alpha = ((...) -> beta) -> beta) -> beta.)
For a more intuitive explanation: Think again of f as a machine that transforms milk into yogurt. We can clone (make a copy) of this machine and try to feed the first machine with the second, but it won't work.
fn f => fn x => (f x, f(x,x))Intuitively, the type of f should be compatible with both the inputs x and (x,x). Namely, we should have at the same time f: alpha -> beta and f: alpha * alpha -> gamma. This would be possible only if the equation alpha = alpha * alpha were solvable, but this is not the case for finite types (a set cannot be equal to the cartesian product of the same set with itself). Note the analogy with the numeric equation x = x + 1, which is unsolvable (for finite numbers) for the same reason.
Note that if we write in ML the above expressions, we get a type error.
(alpha -> beta) -> beta (alpha -> beta) -> alpha beta -> (alpha -> beta) -> alpha alpha -> alpha * betaIf we try to construct an expression for any of these types, we won't succeed. Consider for instance the first type. We have a machine that tranforms milk into yogurt, but we don't have any milk. How can we obtain yogurt? The answer is "We can't". (Feeding the machine with (a copy of) itself does not work, as seen before. Selling the machine and using the money to buy yogurt is not allowed :-)
For instance, the type (alpha -> beta) -> beta above is not a tautology and in fact it is empty. The type alpha -> (alpha -> beta) -> beta, on the contrary, is inhabited, and in fact it is a tautology.
TheoremA type is inhabited (i.e. not empty) only if it corresponds to a logically valid formula (i.e. a tautology).
Hence we can give the following criterion for the emptyness of a type t:
if t is not a tautology, then we know that t is emptyThe reverse does not hold: there are types which are tautologies, but still are empty. One example of such a type is
((alpha -> alpha) -> alpha) -> alphaIn the sub-theory of Classical Logic called Intuitionistic Logic, however, the correspondence is complete. We have in fact that:
A type is inhabited (i.e. not empty) if and only if it corresponds to a formula intuitionistically valid.Sub-theory here means that less formulae are valid.
fn x => xThe type of this expression is
alpha -> alphaLet us call f the function represented by the above expression. Clearly, we can use f also with less general (i.e. more instantiated) types, like for instance pairs, or functions. This means that f can be regarded also as a function of the follwing types:
beta * gamma -> beta * gamma (beta -> gamma) -> beta ->gamma beta list -> beta list ...The difference between alpha -> alpha and the types above is that alpha -> alpha is more general: each type above can be obtained by replacing (instantiating) alpha with a more specific type (respectively, with beta * gamma, beta -> gamma and beta list). It is possible to prove that alpha -> alpha is the most general (or principal) type of the above expression. Any type for that expression can be obtained by intantiating the type alpha -> alpha.
This result is nice because it allows us to consider only the most general type t of an expression: if later, when we use the expression in a certain context, we need an other type, we know we can just derive it from t. The ML type system always derives the most general type of an expression.
TheoremIf an expression has a type, then it has a most general type.
In general, when we construct the type of an expression, we derive first the structure s of the type, and then, by analysing the expression, a set of equations E between type variables. The final type is then obtained by instantiating all the type variables in s so to reflect the constraints imposed by the equations.
More precisely, in order to make sure that we do not miss any constraint, and that we do not impose constraints that aren't necessary, we should instantiate s by using the "most general solution" of the set of equations E. A solution for E is an association between type variables and types which validates all the equations.
A solution for E can be expressed as a system of equations E' in solved form, i.e. such that the left hand sides of each equation is a distinct type variable, and the right hand sides are type expressions containing none of the variables of the lhs. We say that a solution E' for E is most general if E' is equivalent to E, i.e. E and E' have exactly the same solutions.
The most general solution for E can be obtained by repeatedly performing the following operations on E:
alpha -> beta = gamma -> delta * epsilonshould be replaced by the two equations
alpha = gamma beta = delta * epsilon
alpha = gamma -> delta * epsilonthen we should replace alpha by gamma -> delta * epsilon in every other equations
fn f => fn x => (f (f x))Its type will have the following structure s:
alpha -> beta -> gammaWith f: alpha, x: beta, and ((f (f x)): gamma. The constraints, which we derive from the form of the result ((f (f x)), are expressed by the following system of equations E:
alpha = beta -> epsilon, where (f x): epsilon, and alpha = epsilon -> gammaThe most general solution of this set of equations can be obtained by first replacing alpha by beta -> epsilon in the other equation:
alpha = beta -> epsilon beta -> epsilon = epsilon -> gammathen simplify the second equation:
alpha = beta -> epsilon beta = epsilon epsilon = gammathen, replace epsilon by gamma (third equation) in the other equations:
alpha = beta -> gamma beta = gamma epsilon = gammaFinally, replace beta by gamma (second equation) in the other equations:
alpha = gamma -> gamma beta = gamma epsilon = gammaThis final system is in solved form, hence it is the most general solution of E. By applying this solution to s we obtain the most general type, i.e.
(gamma -> gamma) -> gamma -> gammaNote that, when we solve a system of equations, there are in general several ways to proceed, depending on the choice of the particular equation considered at every step. Different choices bring to different (but equivalent) formulation of the result, and to different names in the final type. This does not matter, since the names of type variables are not important.
fun f x = ewithout recursion. In order to derive the type of f we can transform this declaration into the equivalent val declaration:
val f = fn x => enow we can derive the type of fn x => e in the way seen before. This will be also the type of f.
With a little bit of fantasy, we can apply the reasoning directly, without the need of transforming the fun declaration into a val declaration.
fun f v  = v | f v (x::l) = (x,x) :: f is curried and takes two arguments, hence the structure s of its type is
alpha -> beta -> gammaWe have the following constraints:
gamma = alpha, from the result in the fst line beta = delta list, from the pattern of the snd arg in the fst line gamma = (phi * phi) list, from the result in the snd line (where x: phi) delta = phi, from the pattern of the snd arg in the snd lineA solved form of this system is
beta = phi list, gamma = (phi * phi) list, alpha = (phi * phi) list, delta = phi,Which, applied to s, gives the type
(phi * phi) list -> phi list -> (phi * phi) list
fun f v  = [v] | f v (x::l) = f x lf is curried and takes two arguments, hence the structure s of its type is
alpha -> beta -> gammaWe have the following constraints:
gamma = alpha list, from the result in the fst line beta = delta list, from the pattern of the snd arg in the fst line delta = alpha, from the recursive call in the snd lineA solution is
gamma = alpha list beta = alpha list delta = alphawhich, applied to s, gives the type
alpha -> alpha list -> alpha list
The type system consists in a set of rules which define, inductively, the relation
r |- e : t (e has type t under the assumptions r)where:
Note the analogy of the type statement r |- e : t and the evaluation statement env |- e eval t in past lecture notes. Due to this analogy, the type system is sometimes called "static semantics", and r is called "static environment". The roles of r and env are very similar, and their behaviors define the same scoping rules.
Exp ::= Ide (identifiers) | Exp Exp (functional application) | fn Ide => Exp (functional abstraction 1) | fn (Ide,Ide) => Exp (functional abstraction 2) | (Exp,Exp) (pairing) | Exp :: Exp (cons on lists) | hd Exp (head of a list) | tl Exp (tail of a list) | nil (empty list) Type ::= TVar (type variables, i.e. parametres for types) | Type * Type (Cartesian product, the type of pairs) | Type -> Type (functional type, the type of functions) | Type list (the type of lists)
(ide) ------------------ (where x is an identifier) r |- x : r(x) r |- e1 : alpha -> beta r |- e2 : alpha (app) ---------------------------------------------- r |- (e1 e2) : beta r[x:alpha] |- e : beta (abs1) ---------------------------------- r |- (fn x => e) : alpha -> beta
r[x:alpha][y:beta] |- e : gamma (abs2) ----------------------------------------------- r |- fn (x,y) => e : (alpha * beta) -> gamma r |- e1 : alpha r |- e2 : beta (pair) --------------------------------------- r |- (e1,e2) : alpha * beta
r |- e1 : alpha r |- e2 : alpha list (cons) ------------------------------------------- r |- (e1 :: e2) : alpha list r |- e : alpha list (head) ----------------------- r |- (hd e) : alpha r |- e : alpha list (tail) ---------------------------- r |- (tl e) : alpha list (nil) ---------------------------- r |- nil : alpha list
We say that e has type t if we can derive the statement emptyset |- e : t.
In order to derive the most general type of an expression, we need to construct a proof for the statement emptyset |- e : alpha, and then instantiate alpha with the solution of the constraint that we find in this proof (i.e. the constraints imposed by the particular form of the types in the conclusion of the rules) from the var rule. A similar process allows us to derive an expression for a given type t.