COS 441 - Type Checking - April 9, 1996

Streams Revisited

The following macro should make streams a little easier to use.

(extend-syntax ($cons)
  (($cons a b)
   (cons a (make-stream (lambda () b)))))
Now functions like $append and tree->stream are easy:
(define $append
  (lambda (a b)
    (cond ((null? a) b)
          ((null? b) a)
          (else ($cons ($car a) ($append ($cdr a) b))))))
(define tree->stream
  (lambda (t)
    (if (symbol? t)
        ($cons t $nil)
        ($append (tree-stream (car t)) (tree->stream (cdr t)))))

Static Types

What is (+ 1 #t)? Assuming that + is bound as usual, it is an error because + is supposed to be applied to numbers. That is, the input domain for arguments to + is the set of all numbers. We call such a set a type. The expression (+ 1 #t) is a type error.

Scheme ensures that arguments to primitive operations belong to appropriate types by performing run-time type checks. That is + is defined as:

(define +
  (lambda (x y)
    (if (and (number? x) (number? y))
        (really+ x y)   
        (error "arg to + not a number"))))
Languages like Scheme that deal with types in this way are called latently typed or dynamically typed.

A correct program surely contains no type errors. But Scheme provides no tools to help us find type errors. A static type system provides such a tool.

Explicit Types

In a statically typed language like Pascal, all names have explicitly declared types. Pascal's static type system employs a set of rules to check that functions are applied only to arguments of correct type. Hence the compiler will only accept programs that have no type errors.

function f(x : int) : int
        f := 0 + true;
The above Pascal program is rejected by the compiler.

Let's design a Pascal-like static system for a Scheme-like language.

e ::= c | x | (lambda (x : t) e) | (e e)
c ::= #f | #t | numbers | succ | zero?
This language requires the programmer to state the type of formal parameters explicitly, like Pascal. (We leave out the return types though.) Types are defined inductively as follows:
t ::= bool | number | (t -> t)
We use a framework somewhat similar to natural semantics. There are inference rules concluding with type judgments that express what type an expression has, rather what value it has. We need an environment A to assign types to free variables. Thus A is a map from vars to types. The judgment:
    A |- e : t
can be read as "in type environment A, expression e has type t." Like natural semantics, we need a rule for each expression form. (But unlike natural semantics, these rules are defined inductively on the structure of expressions.)


A |- #t : bool 
A |- #f : bool
A |- n : number
A |- succ : number -> number
A |- zero? : number -> bool


A(x) = t
A |- x : t


A[x |-> t] |- e : t'
A |- (lambda (x : t) e) : (t -> t')


A |- e[1] : t' -> t    A |- e[2] : t'
A |- (e[1] e[2]) : t


A |- e[1] : bool   A |- e[2] : t    A |- e[3] : t
A |- (if e[1] e[2] e[3]) : t
Now let's implement a procedure that, given a program, either returns a type, or signals an error if the program is not typable.
(define type-check
  (lambda (e A)
    (variant-case e
      (Const (value) (cond ((number? value) 'number)
                           ((boolean? value) 'bool)
                           ((eq? 'succ value) '(number -> number))
      (Var (name) (lookup A name))
      (Lam (name type body) 
           (let ((body-type (type-check body (extend A name type))))
             (list type '-> body-type)))
      (Ap (fun arg) 
          (let ((tf (type-check fun A))
                (ta (type-check arg A)))
            (if (and (arrow? tf) (matches ta (dom tf)))
                (ran tf)
                (error "type error")))))))
(define arrow?
  (lambda (t)
    (and (list? t) (= 3 (length t)) (eq? '-> (cadr t))))

(define dom car)
(define ran caddr)
(define matches equal?)   
Does this mean that type correct Pascal programs never have errors? No : consider the expression (N / 0). This is not considered a type error. Why? Because / would have to accept "all numbers" for the first argument and "all numbers but zero" for the second. Designing a type system with such types is not feasible.

Are all type incorrect programs wrong? No consider: (if #t 1 (+ 1 #t)) This is rejected by our static type system even though it executes fine. We will see better examples of this later.

Implicit Typing

Can we get away without the explicit type for function parameters? That is, build a static type system for a language with Scheme syntax? To do this we need a new type checking rule for lambda.

A[x |-> t] |- e : t'
A |- (lambda (x) e) : t -> t'
Consider (lambda (x) (lambda (y) (y))). We can construct a type derivation that proves this expression is typable if we first guess its type. Suppose we guess (number -> number). Then we can construct a tree derivation establishing:
0 |- ((lambda (x) x) (lambda (y) y))) : (number -> number)
Similarly if we guess (bool -> bool), we can construct a type derivation that proves this as the type. But if we guess (bool -> number), we are unable to construct a proof. We want a procedure that will automatically construct all possible types that an expression e can have in some type environment.

We represent guesses with type variables, a. Hence we add type vars to the syntax of types.

t ::= ... | a
We use type vars when we are forced to guess a type. Since lambda arguments no longer have explicit types, this is where we are forced to guess.
0[x |- (a -> a)](x) = (a -> a)                    0[y |-> a](y) = a
--------------------------------                  ------------------
0[x |- (a -> a)] |- x : (a -> a)                  0[y -> a] |- y : a
-----------------------------------------       ------------------------------
0 |- (lambda (x) : ((a -> a) -> (a -> a))       0 |- (lambda (y) y) : (a -> a)
0 |- (lambda (x) x) (lambda (y) y)) : (a -> a)