11 — Types & Proofs
Tuesday, 11 February 2020
Presenters (1) Anthony Beetem, Jack Gelinas (2) Anthony Gavazzi, Kaitlyn O’Donnell
Around 1960 they were demands to the compiler that it allocate a certain number of bits, bytes, and words. These days they are also suggestions to the IDE to help in certain ways, say, with name completion, method suggestions, or refactorings.
Let’s call these physical types.
A second idea that emerged around 10 years later is that types were claims about code that can be validated without running the code.
The relationship between claims (formal statements about a slice of the world) and truth (they actually hold up in this world) is called soundness.
If the validation process is sound, types also prevent certain error situations during program execution. In this case, types are “perfect waethermen.”If it isn’t, there is nothing that types predict about execution.
Let’s call these logical types.
Both ideas are important for the daily work of a developer, and they are related. But, the first can be realized without the second and, in practice, it often is. Realizing the first one is often quite easy and ad hoc, that is, there is little systematic about it and so we don’t say anything. Getting the second one right is hard and may have a larger impact than the first on productivity and debugging. It is also something that whose universality needs a conversation.
The Language of Types
We need to agree on what kind of claims we wish to make about the execution of programs. In PL, we call the collection of these claims types.
The word structural has exactly the same meaning as in the text books for Fundamentals I and II. At first we will look at simple types that are structural, which means we don’t care about the name of the type but only its atomic pieces and how they relate to each other.
Types See figure 47 for the definition of types.
Lectures/11/types.rkt
#lang racket (struct int [] #:transparent) (struct -> [domain range] #:transparent) ;; - - - - - - - - - - - - - - - - - - - - - - - - - #; {Type is one of: -- (int) ;; represents the idea of integer -- (-> Type Type)} ;; .. the idea of function ;; - - - - - - - - - - - - - - - - - - - - - - - - - (define type=? equal?) (provide (struct-out int) (struct-out ->) #; {Type Type -> Boolean} type=?)
; the integer type (int) ; functions from integers to integers (-> (int) (int)) ; functions from integers to functions from integers to integers (-> (int) (-> (int) (int))) ; functions from functions from integers to integers to integers (-> (-> (int) (int)) (int))
Alternative
Nominal types are common with object-oriented languages.
But, nominal types are also imposing stringent constraints on software development. They often force developers to copy a piece of code simply to accommodate a type with a different name and the same structure. This downside is well recognized. The argument of supporters is that nomimal types are easy to implement and understand for everybody.
Typed Syntax
Validating types about programs needs type statements about the program.
In PL, we conventionally specify the types of variables, which may not
sound like making a claim. It turns out the claim is implicit—
So let’s add types to the syntax of our language. We equip every
variable—
Lectures/11/isl-as-data.rkt
#lang racket ;; data representation (AST) of a typed variant of ISl (struct tnode [op left right] #:transparent) (struct tfun* [parameter type body] #:transparent) (struct tcall [fname argument] #:transparent) (struct tif-0 [tst thn els] #:transparent) ;; - - - - - - - - - - - - - - - - - - - - - - - - - #; {TypedISL = Int || Var || (tnode O TypedISL TypedISL) || (tfun* Var Type TypedISL) || (tcall TypedISL TypedISL) || (tif-0 TypedISL TypedISL)} #; {O = + || *} #; {Var = String} ;; - - - - - - - - - - - - - - - - - - - - - - - - - (provide (struct-out tnode) (struct-out tif-0) (struct-out tfun*) (struct-out tcall))
Typing Rules, Proof Rules
The connection between typing rules and logical proof rules—
or types and proofs— was discovered over the course of a decade. The major contributors were Curry, Howard, Morris, Per-Lof, Reynolds, Mitchell, and Plotkin. People now often speak of the Curry-Howard isomorphism (a syntactic or a semantic one, or sometimes w/o adjective).
To prove the implied claims, we compute types for every single
sub-expression in a program—
Over time, the sub-discipline of types research has adopted tools from logic to state how type specifications for variables imply types for the expressions in the program. Specifically people use proof rules.
A TEnv is a sequence of variable-type associations. We can add one to
the right to make a new TEnv, and we look up from the right to find an
association.
for n an Integer
------------------
TEnv |- n : (int)
(x,t) in TEnv (from the right)
-------------------------------
TEnv |- x : t
TEnv + (x,t) |- body : t*
--------------------------------------
TEnv |- [tfun x t body] : [-> t t*]
TEnv |- f : [-> t* t] TEnv |- a : t*
--------------------------------------------
TEnv |- [tcall f a] : t
How do you use such rules? You start with a program P and an empty TEnv. The two make half a claim: TEnv |- P : ___. The goal is to find out what type to put into this hole. We use the rules as follows:
You check which construct is the top-most one and you use the matching rule to derive new claims. Not all rules construct more claims; in that case, you’re done. These "finishing" rules also give you a type. If there are any open claims, work on those. If not, you have a proof tree and you know the type of the program. You may also discover unresolvable conflicts, and that you know that the claims are wrong.
STEP 1: 0 |- [tcall [fun x (int) x] o] : ____
STEP 2: there's one open claim, and one rule to pick
0 |- [fun x (int) x] : ____ 0 |- 0 : _____
----------------------------------------------------
0 |- [tcall [fun x (int) x] o] : ____
STEP 3: there's two open claims. Pick the easy one.
0 is an integer
------------------
0 |- [fun x (int) x] : ____ 0 |- 0 : (int)
----------------------------------------------------
0 |- [tcall [fun x (int) x] o] : ____
STEP 4: there's one open claim, and there's only one rule.
[x, (int)] |- x : __________ 0 is an integer
---------------------------- ------------------
0 |- [fun x (int) x] : (-> (int) ___) 0 |- 0 : (int)
------------------------------------------------------------
0 |- [tcall [fun x (int) x] o] : ____
STEP 5: there's one open claim, and there's only one rule.
And the use of this rule allows us to fill in the last type hole.
[x, (int)] is in the TEnv
----------------------------
[x, (int)] |- x : (int) 0 is an integer
---------------------------- ------------------
0 |- [fun x (int) x] : (-> (int) (int)) 0 |- 0 : (int)
------------------------------------------------------------
0 |- [tcall [fun x (int) x] o] : (int)
The tcall rule has an implicit check: the domain of the arrow type and the argument’s type have to be the same.
The tfun rule shows how, under the assumption that x has a type, the body must have a certain type.
[tcall [fun x (int) x] [fun x (int) x]] |
Type Checking
It’s straightforward to translate these rules into code. And that is called a type checker.
Lectures/11/type-check.rkt
#lang racket (require "types.rkt") (require "isl-as-data.rkt") (require "../6/environment.rkt") ;; - - - - - - - - - - - - - - - - - - - - - - - - - (define UNDECLARED "undeclared variable") (define ARITHMETIC "bad types for prim op") (define DOMAIN "either f is not an -> or domain type doesn't match arg type") #; {TypedISL -> Type} (define (type-check isl0) #; {TypedISL TEnv -> Type} (define (type-check/accu isl env) (match isl [(? string?) (if (defined? isl env) (lookup isl env) (error 'tc UNDECLARED))] [(? integer?) (int)] [(tnode o l r) (define tl (type-check/accu l env)) (define tr (type-check/accu r env)) (if (and (type=? tl (int)) (type=? tr (int))) (int) (error 'tc ARITHMETIC))] [(tfun* x t b) (define env+ (add x t env)) (define tbdy (type-check/accu b env+)) (-> t tbdy)] [(tcall f a) (define tf (type-check/accu f env)) (define ta (type-check/accu a env)) (if (and (->? tf) (type=? (->-domain tf) ta)) (->-range tf) (error 'tc DOMAIN))])) (type-check/accu isl0 empty)) ;; - - - - - - - - - - - - - - - - - - - - - - - - - (provide type-check)
Stop! Design a rule for tif-0 and then add it to the type checker.