11 — Types & Proofs

Tuesday, 11 February 2020

Presenters (1) Anthony Beetem, Jack Gelinas (2) Anthony Gavazzi, Kaitlyn O’Donnell

Types come in two flavors:
  • 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.


  #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}

Figure 47: The Language of Types

; the integer type
; 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))
Yes, this very sentence appears in the text book of Fundamentals I. Stop! Can you make examples of these three things?


An alternative to structural is nominal, which means we identify types only if they have the same name. Let’s imagine we have a way of naming types, say, define-named-type. Then we could say this:
(define-named-type int-2  (-> (int) (int)))
(define-named-type binary (-> (int) (int)))
Although the two named types have the same structure (same basic two pieces connected in the same way), a nominal type system would not treat them the same—because they have different names.

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—it is about the other pieces of the program and how they work together.

So let’s add types to the syntax of our language. We equip every variable—declared or parameter—with a type specification. We call this TypedISL.


  #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}
  ;; - - - - - - - - - - - - - - - - - - - - - - - - -
   (struct-out tnode)
   (struct-out tif-0)
   (struct-out tfun*)
   (struct-out tcall))

Figure 48: Typed Program Syntax

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—a process called type checking.

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



   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

Figure 49: Simple Structural Typing Rules

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)


Figure 50: Using Typing Rules

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.

Stop! Try to play the "game" of figure 50 for the program

  [tcall [fun x (int) x] [fun x (int) x]]

What happens when you do?

Type Checking

It’s straightforward to translate these rules into code. And that is called a type checker.


  #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)))
                           (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)

Figure 51: Algorithmic Type Checking

Stop! Design a rule for tif-0 and then add it to the type checker.