Currying

The term "currying" comes from Haskell Curry, a famous logician who worked in State College (Dept of Mathematics) and developed the Combinatory Logic, which, together with the Lambda Calculus (Alonzo Church) constitutes the foundation of Higher Order Functional Programming.

Essentially, the idea is the following: consider a function f: 'a * 'b -> 'c. This function takes a pair of arguments, the first of type 'a, and the second of type 'b, and gives back a result in 'c. There is only one way f can be applied: by passing the two arguments together. Given x:'a, and y:'b, we write f(x,y) for the application of f to the pair(x,y).

We could imagine now a variant of f that, instead of taking the arguments together, takes them "one at the time", and gives the same result as f when it is supplied with both arguments. This variant is called curried version of f. Let us denote it by fc. The type of this function is fc: 'a -> 'b -> 'c, and, by definition, we have that for every x:'a, and y:'b, f(x,y) = fc x y holds. The main difference between f and fc is that the latter can be applied also to the first argument only: The expression fc x (for x:'a) is perfectly legal and denotes a function of type 'b -> 'c (the function which, when provided with an input y:'b, will give as result f(x,y)).

In order to provide support for curried functions, a language needs to be Higher Order. The currying adds flexibility and expressivity to the language, and it is part of the general principle of abstraction. In a sense, currying is intrinsic to the philosophy and design of an higher-order language. Below we will see some examples of how this feature adds expressivity.

Let us see how the currying possibility can be used in ML.

Defining curried functions

Let us consider a function with two arguments like the append of two lists. A possible definition is:
```   - fun append([],k) = k
| append(x::l,k) = x::append(l,k);
val append = fn : 'a list * 'a list -> 'a list
```
The curried version of append can be defined in the following way:
```   - fun append_c [] k = k
| append_c (x::l) k = x::(append_c l k);
val append_c = fn : 'a list -> 'a list -> 'a list
```
or, equivalently:
```   - fun append_c [] = (fn k => k)
| append_c (x::l) = fn k => x::(append_c l k);
val append_c = fn : 'a list -> 'a list -> 'a list
```
We can now write expressions like append_c [1], for instance in a declaration:
```   - val append_one = append_c [1];
val append_one = fn : int list -> int list
```
Note that the system does not evaluate the expression append_c [1], because it is a function. It only computes its type. The evaluation will be performed only when we provide also the second argument. For instance:
```   - append_one [5,6];
val it = [1,5,6] : int list
```

Using currying for abstraction

We illustrate here how Higher Order and currying provide powerful and elegant mechanisms for abstraction.

Consider the functions sum_all : int list -> int and product_all : int list -> int (respectively sum and product of all the elements in a list of integers). They can be defined as follows:

```   - fun sum_all [] = 0
| sum_all (x::l) = x + sum_all l;
val sum_all = fn : int list -> int

- fun product_all [] = 1
| product_all (x::l) = x * product_all l;
val product_all = fn : int list -> int
```
Note that these two function work according to the same scheme: they scan the list (recursively) element by element, and perform a certain operation on every element (and on the result of the recursive call), and give a certain initial result when the list is empty.

This scheme is common to several other functions. We could then think of defining an abstract function (abstract wrt the operation and the initial element), which represent the general scheme. The particular functions (like sum_all and product_all can then be defined by providing the particular operation and initial value. This general function is commonly called reduce, and it is "more natural" to define it by using currying, as follows:

```   fun reduce f v [] = v
| reduce f v (x::l) = f(x, reduce f v l);
val reduce = fn : ('a * 'b -> 'b) -> 'b -> 'a list -> 'b
```
Here, f: 'a * 'b -> 'b represents the operation, and v:'b represents the initial value.

The definitions of sum_all and product_all can now be given as follows:

```   - val sum_all = reduce (op +) 0;
val sum_all = fn : int list -> int

-  val product_all = reduce (op * ) 1;
val product_all = fn : int list -> int
```
We need to use (op +) and (op * ) instead of + and * because the latter are infix, while in the definition of reduce the parameter f is prefix. The operator op changes a function from infix to prefix.

We can now apply these functions to lists of integers, as illustrated in the following examples:

```   - sum_all [];
val it = 0 : int
- sum_all [1,2,3,4];
val it = 10 : int

- product_all [];
val it = 1 : int
- product_all [1,2,3,4];
val it = 24 : int
```
Another example of function that we can define by using reduce is the function forall, which checks whether a certain property p: 'a -> bool holds for all elements of a list (of type 'a list). We can define it as follows:
```   fun forall p = let fun f(x,b) = p x andalso b
in reduce f true
end;
val forall = fn : ('a -> bool) -> 'a list -> bool
```
Examples of uses (note that (fn x => x>0) represents the property of being positive; (fn x => x mod 2 = 0) represents the property of being even).
```   - forall (fn x => x>0) [1,2,3];
val it = true : bool
- forall (fn x => x>0) [~1,2,3];
val it = false : bool

- forall (fn x => x mod 2 = 0) [2,0,4];
val it = true : bool
- forall (fn x => x mod 2 = 0) [2,1];
val it = false : bool
```
Other examples can be found in Assignment 8 (CSE 428, Spring 99). Note that in the assignment the type of reduce is restricted. The reason is due to a certain limitation of the type system of the present implementation of SML. We won't go into that because it's a bit complicated, and not so important.

The concept of currying extends naturally to arbitrary tuples of arguments.

Type theory and programming languages

Types are very useful in programming languages. The advantages of using a typed language include:
1. (substantial) help in detecting errors
3. increasing efficiency in implementation (because of better allocation of resources)

Some terminology

• Type system: the formal description (theory) of the laws which associate a type to every expression, in a given language.
• Typed language: a language which is provided with a type system. Examples of languages which have a type system: almost all modern high level languages, except Scheme (which is used mainly for didactical purposes) and (most of) Prolog.
• Type checker: implementation of the type system, i.e. component of the implementation of a language which checks the correct use of names and expressions, relatively to types.
• Type inference system: component wich not only checks, but is also able to infer the type of expressions and names, so that they do not need to be declared explicitly by the user. Examples of languages which have a type inference mechanism: ML, and most of the (typed) functional languages.

• Strongly typed: A language is strongly typed if, once the type checker has given its "ok", we can be sure that the program will never give an error due to types at run time. Examples: Pascal, ML.
• Weakly typed: The contrary of strongly typed. Some languages give up the strongly typed property (on certain data types) in favour of flexibility. For instance, Algol, C, C++ etc. do not check that indexes of arrays are used within their range.

• Static type checking: we say that a language is statically type-checked if the type checking done entirely at compile time. Examples: Pascal, ML.
• Dynamic type checking: the part of type checking done at run time. In general it is preferable to do the type checking at compile time, since it is more efficient. From the point of view of flexibility, however, we might want to allow that the type correctness of an expression depends on run-time properties. An example is the operation of casting in C++. Note that the price to be paid for this flexibility includes safety: we can never be sure that a program is type safe when correctness depends on run-time properties (it might run correctly for years and then, one day, give a type error).

• Polymorphic type system: A type system which allow types to depend on parameters. This feature is a powerful mechanism for abstraction. Note: by "polymorphism" here we mean "generic polymorphism", as distinct from "ad-hoc polymorphism". The latter concept merely means that an operation is overloaded, and has nothing to do with parametricity. Examples of languages with (generic) polymorphism: ML and most of the (typed) functional languages. C++ also allows to express some type parametricity (templates), but it is not well integrated in the type system. (The type checker of C++ does not check that templates are used consistently, they are expanded like macros at run time.) We will study the type system of ML, as the case study for a type system, because
1. it has solid mathematical foundations
2. it is well implemented
3. it is rich (higher-order, polymorphism)
By the way, there is a recent project which aims at enhancing Java with a polymorphic type system. They are taking ML as model (and not C++, because, as explained above, in C++ polymorphism is not done so well). This project is carried on in collaboration by a team at Sun Micro Sys. and a team at Bell Labs, and it's leaded by Philip Wadler. Currently, their main obstacle is how to make the new Java (called "Generic Java") compatible with the current version.

The type system of ML

In order to focus on the main concepts regarding types, we will consider a small subset of ML and its type system. The expressions in this subset, which we will call Mini-ML, are described by the following grammar:
```    Exp ::= Ide                 (identifiers)
| Exp Exp             (functional application)
| fn Pattern => Exp   (functional abstraction)
| (Exp,Exp)           (pairing)
| Exp :: Exp          (cons on lists)
| hd Exp              (head of a list)
| tl Exp              (tail of a list)
| nil                 (empty list)

Pattern ::= Ide
| (Ide,Ide)
```
This is a very small subset of ML (both wrt the expressions and the patterns), and we may wish to extend it later, so to include other interesting data types and constants (like numbers). For the moment however we prefer focussing on just few constructs.

The types of this language constitute a language (type expressions) described by the following grammar:

```   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)
```
We will use the Greek letters alpha, beta, gamma,... to represent the type variables. They correspond to the dashed symbols 'a, 'b, 'c, ... used by the ML system.

Convention: * is left associative. -> is right associative. list has precedence wrt *, and * has precedence wrt ->.

Before studying the Type System formally, let us see some examples of type inferences that are done automatically by the type system of ML.

1. ```- fn f => fn x => fn y => f x y;
val it = fn : ('a -> 'b -> 'c) -> 'a -> 'b -> 'c
```
2. ```- fn f => fn (x,y) => f(x,y);
val it = fn : ('a * 'b -> 'c) -> 'a * 'b -> 'c
```
3. ```- fn f => fn x => fn y => (f x,f y);
val it = fn : ('a -> 'b) -> 'a -> 'a -> 'b * 'b
```
4. ```- fn l => fn f => (f (hd l)) :: (tl l);
val it = fn : 'a list -> ('a -> 'a) -> 'a list
```
Let us see what is, intuitively, the "reasoning" done by the ML type system to derive the above types.

1. fn f => fn x => fn y => f x y is a function, so its type must be of the form
```   alpha -> beta -> gamma -> delta
```
where alpha is the type of f, beta is the type of x, gamma is the type of y, and delta is the type of the result.

Now, let us analyze how these types are related. In the resulting expression, f is applied to x and the result is applied to y (remember that f x y = (f x) y because application is left-associative). Hence alpha = beta -> phi where phi is the type of (f x). Since we then apply (f x) to y, and we have called delta the type of the result, we must have phi = gamma -> delta.

In conclusion the type is:

```   (beta -> gamma -> delta) -> beta -> gamma -> delta
```
(names are not important, only their relation is)

We need to put parentheses around the first beta -> gamma -> delta because if we don't do that then the type becomes

```   beta -> gamma -> delta -> beta -> gamma -> delta
```
which is interpreted as
```   beta -> (gamma -> (delta -> (beta -> (gamma -> delta))))
```
since -> is right-associative.

2. Convention: From now on I'll use the notation "z: t" to mean "z has type t".

fn f => fn (x,y) => f(x,y) is a function, where the second parameter is a pair, hence its type must be of the form

```   alpha -> (beta * gamma) -> delta
```
where f: alpha, x: beta, y: gamma, and (f(x,y)): delta

Since f is applied to (x,y) and the result is of type delta, then it must be

```   alpha = (beta * gamma) -> delta
```
hence the type is
```   ((beta * gamma) -> delta) -> (beta * gamma) -> delta
```
(the parentheses around (beta * gamma) are not necessary because * has priority wrt ->)

3. fn f => fn x => fn y => (f x,f y) is a function, hence its type must be of form
```   alpha -> beta -> gamma -> delta
```
where f: alpha, x: beta, y: gamma, and (f x, f y): delta

Now, since the result is a pair, we must have delta = phi * psi, where (f x): phi and (f y): psi. Since f is applied to x and to y, we must have alpha = beta -> phi, and also alpha = gamma -> psi. Hence we obtain beta = gamma, and phi = psi. Thus the type is (again, names are not important):

```  (beta -> phi) -> beta -> beta -> phi * phi
```

4. fn l => fn f => (f (hd l)) :: (tl l) is a function, hence its type must be of form
```   alpha -> beta -> gamma
```
where l: alpha, f: beta, and ((f (hd l)) :: (tl l)) : gamma.

Since the result is constructed with a cons operation, it must be a list. Hence we have gamma = delta list, where delta is the type of (f (hd l)). Furthermore, the rest of the list is given by (tl l), hence also l must have type delta list (l and (tl l) have the same type). Therefore we have alpha = delta list. Finally, observe that (hd l): delta and (f (hd l)): delta, hence we can deduce f: delta -> delta. In conclusion, the resulting type is:

```   delta list -> (delta -> delta) -> delta list
```

Constructing an expression with a given type

We have seen how to derive the type of a given expression. By reversing the reasoning, we can do the opposite, namely, given a type, we can derive an expression with that type.

Example

Consider the type
```      alpha -> (alpha -> beta) -> beta
```
Let 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 it
i.e. use f with input x. Hence, the resulting expression is:
```      fn x => fn f => f x
```

Example

Consider the type
```      (alpha * beta -> gamma) -> alpha -> beta -> gamma
```
An 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)
```

Expressions without type

Not all expressions have a type.

Example

One of the simplest cases is the expression
```      fn f => f f
```
Let us see why it does not have a type. If it did, it should be something like
```      (alpha -> beta) -> beta
```
where 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.

Example

Another example of an expression without type is
```      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.

Types which do not correspond to any expression

There are also types which are not the type of any expression. They are called "empty types" or "types which are not inhabited". Examples of such types are:
```      (alpha -> beta) -> beta
(alpha -> beta) -> alpha
beta -> (alpha -> beta) -> alpha
alpha -> alpha * beta
```
If 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 :-)

A criterion for empty types

There is an extremely interesting analogy between type theory and logics, which is known under the name of "Curry-Howard isomorphism". Basically the idea is that we can consider types as logical formulas. (Types here are meant as restricted to the types we can construct with -> and *.) More precisely:
• -> corresponds to implication
• * corresponds to conjunction
• Type variables correspond to propositions
An important result is the following:

Theorem

A type is inhabited (i.e. not empty) only if it corresponds to a logically valid formula (i.e. a tautology).
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.

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 empty
The reverse does not hold: there are types which are tautologies, but still are empty. One example of such a type is
```      ((alpha -> alpha) -> alpha) -> alpha
```
In 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.

Most general type

Consider an expression like
```      fn x => x
```
The type of this expression is
```      alpha -> alpha
```
Let 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.

Theorem

If an expression has a type, then it has a most general type.
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.

How to compute the most general type

In all examples we have seen so far, the type we had constructed was the most general type. Let us see more formally how to do that.

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:

• Simplify the equations. For instance, an equation
```   alpha -> beta = gamma -> delta * epsilon
```
should be replaced by the two equations
```   alpha = gamma
beta = delta * epsilon
```
• Substitute the variables with their solution. For instance, if we have the equation
```   alpha = gamma -> delta * epsilon
```
then we should replace alpha by gamma -> delta * epsilon in every other equations
If we succeed to transform E into a system in solved form, then we have found the most general solution (success). If we find an equation of the form alpha = ... alpha ... (i.e. the rhs contains alpha and something else), then we stop with failure, because we know that alpha = ... alpha ... is unsolvable unless the rhs is exactly alpha. It is possible to prove that we always stop, either with success or with failure.

Example

Consider the expression
```      fn f => fn x => (f (f x))
```
Its type will have the following structure s:
```      alpha -> beta -> gamma
```
With 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 -> gamma
```
The 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 -> gamma
```
then simplify the second equation:
```
alpha = beta -> epsilon
beta = epsilon
epsilon = gamma
```
then, replace epsilon by gamma (third equation) in the other equations:
```
alpha = beta -> gamma
beta = gamma
epsilon = gamma
```
Finally, replace beta by gamma (second equation) in the other equations:
```
alpha = gamma -> gamma
beta = gamma
epsilon = gamma
```
This 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 -> gamma
```
Note 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.

Declaration of functions: how to derive their type

Consider a definition of a function of the form
```      fun f x = e
```
without recursion. In order to derive the type of f we can transform this declaration into the equivalent val declaration:
```      val f = fn x => e
```
now 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.

Functions defined by pattern matching

When we want to derive the type of a function defined by pattern matching, the reasoning is essentially the same as above. The only difference is that we may need to add some constraints coming from the patterns.

Example

Consider the following function f:
```      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 -> gamma
```
We 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 line
```
A 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
```

Recursive functions

Suppose that we want to derive the type of a function defined recursively. The reasoning is again the same as above, with the only difference that we need to add some constraints coming from the recursive calls.

Example

```      fun f v [] = [v]
| f v (x::l) = f x l
```
f is curried and takes two arguments, hence the structure s of its type is
```      alpha -> beta -> gamma
```
We 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 line
```
A solution is
```
gamma = alpha list
beta  = alpha list
delta = alpha
```
which, applied to s, gives the type
```      alpha -> alpha list -> alpha list
```

The formal definition of the type system

For the sake of completeness, we give here the formal definition of the type system of Mini ML. This formal definition has not been given in class, and will not be required at the exam. However, it might help understanding better (and more precisely) the relation between expressions and types, which has been explained above at an intuitive level only.

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:
• e is a Mini-ML expression
• t is a Mini-ML type
• r is a set of associations between type-variables and types
we will also use the notation r(x) to represent the type that is associated to x in r, and r[x:t] to represent the addition of the association x:t to r. This addition "shadows" a possible previous association for x in r.

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.

Syntax of Mini-ML expressions and types

We recall here the definition of Mini ML and its types
```    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)
```

Rules of the type system

• Core of Mini ML (corresponding to the lambda calculus)
```(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
```

• Pairs
```              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
```

• Lists
```         r |- e1 : alpha    r |- e2 : alpha list
(cons) -------------------------------------------
r |- (e1 :: e2) : alpha list

r |- e : alpha list
r |- (hd e) : alpha

r |- e : alpha list
(tail) ----------------------------
r |- (tl e) : alpha list

(nil) ----------------------------
r |- nil : alpha list
```

A statement r |- e : t is derivable in the type system if there exists a proof tree for this statement.

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.