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)))))
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.
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 begin f := 0 + true; endThe 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 : tcan 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]) : tNow 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.
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 ::= ... | aWe 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)
(((lambda (x) (lambda (y) (x y))) zero?) 0)
is typable with type bool
((lambda (x : number) (succ x)) #t)
is not typable.