PL: Lecture #25  Wednesday, November 28th
(text file)

Typing Recursion

We already know that without recursion life can be very boring… So we obviously want to be able to have recursive functions — but the question is how will they interact with our type system. One thing that we have seen is that by just having functions we get recursion. This was achieved by the Y combinator function. It seems like the same should apply to our simple typed language. The core of the Y combinator was using an expression similar to Omega that generates the infinite loop that is needed. In our language:

{call {fun {x} {call x x}} {fun {x} {call x x}}}

This expression was impossible to evaluate completely since it never terminates, but it served as a basis for the Y combinator so we need to be able to perform this kind of infinite loop. Now, consider the type of the first x — it’s used in a call expression as a function, so its type must be a function type, say τ₁->τ₂. In addition, its argument is x itself so its type is also τ₁ — this means that we have:

τ₁ -> τ₂ = τ₁

and from this we get:

=> τ₁ = τ₁ -> τ₂
      = (τ₁ -> τ₂) -> τ₂
      = ((τ₁ -> τ₂) -> τ₂) -> τ₂
      = ...

And this is a type that does not exist in our type system, since we can only have finite types. Therefore, we have a proof by contradiction that this expression cannot be typed in our system.

This is closely related to the fact that the typed language we have described so far is “strongly normalizing”: no matter what program you write, it will always terminate! To see this, very informally, consider this language without functions — this is clearly a language where all programs terminate, since the only way to create a loop is through function applications. Now add functions and function application — in the typing rules for the resulting language, each fun creates a function type (creates an arrow), and each function application consumes a function type (deletes one arrow) — since types are finite, the number of arrows is finite, which means that the number of possible applications is finite, so all programs must run in finite time.

Note that when we discussed how to type the Y combinator we needed to use a Rec constructor — something that the current type system has. Using that, we could have easily solve the τ₁ = τ₁ -> τ₂ equation with (Rec τ₁ (τ₁ -> τ₂)).

In the our language, therefore, the halting problem doesn’t even exist, since all programs (that are properly typed) are guaranteed to halt. This property is useful in many real-life situations (consider firewall rules, configuration files, devices with embedded code). But the language that we get is very limited as a result — we really want the power to shoot our feet…

Extending Picky with recursion

As we have seen, our language is strongly normalizing, which means that to get general recursion, we must introduce a new construct (unlike previously, when we didn’t really need one). We can do this as we previously did — by adding a new construct to the language, or we can somehow extend the (sub) language of type descriptions to allow a new kind of type that can be used to solve the τ₁ = τ₁ -> τ₂ equation. An example of this solution would be similar to the Rec type constructor in Typed Racket: a new type constructor that allows a type to refer to itself — and using (Rec τ₁ (τ₁ -> τ₂)) as the solution. However, this complicates things: type descriptions are no longer unique, since we have Num, (Rec this Num), and (Rec this (Rec that Num)) that are all equal.

For simplicity we will now take the first route and add rec — an explicit recursive binder form to the language (as with with, we’re going back to rec rather than bindrec to keep things simple).

First, the new BNF:

<PICKY> ::= <num>
          | <id>
          | { + <PICKY> <PICKY> }
          | { < <PICKY> <PICKY> }
          | { fun { <id> : <TYPE> } : <TYPE> <PICKY> }
          | { call <PICKY> <PICKY> }
          | { with { <id> : <TYPE> <PICKY> } <PICKY> }
          | { rec { <id> : <TYPE> <PICKY> } <PICKY> }
          | { if <PICKY> <PICKY> <PICKY> }

<TYPE>  ::= Number
          | Boolean
          | ( <TYPE> -> <TYPE> )

We now need to add a typing judgment for rec expressions. What should it look like?

            ???
———————————————————————————
Γ ⊢ {rec {x : τ₁ V} E} : τ₂

rec is similar to all the other local binding forms, like with, it can be seen as a combination of a function and an application. So we need to check the two things that those rules checked — first, check that the body expression has the right type assuming that the type annotation given to x is valid:

  Γ[x:=τ₁] ⊢ E : τ₂  ???
———————————————————————————
Γ ⊢ {rec {x : τ₁ V} E} : τ₂

Now, we also want to add the other side — making sure that the τ₁ type annotation is valid:

Γ[x:=τ₁] ⊢ E : τ₂  Γ ⊢ V : τ₁
——————————————————————————————
Γ ⊢ {rec {x : τ₁ V} E} : τ₂

But that will not be possible in general — V is an expression that can include x itself — that’s the whole point. The conclusion is that we should use a similar trick to the one that we used to specify evaluation of recursive binders — the same environment is used for both the named expression and for the body expression:

Γ[x:=τ₁] ⊢ E : τ₂  Γ[x:=τ₁] ⊢ V : τ₁
—————————————————————————————————————
    Γ ⊢ {rec {x : τ₁ V} E} : τ₂

You can also see now that this rule adds an arrow type to the Γ type environment, in a way that makes it possible to use it over and over, making it possible to run infinite loops in this language.

Our complete language specification is below.

<PICKY> ::= <num>
          | <id>
          | { + <PICKY> <PICKY> }
          | { < <PICKY> <PICKY> }
          | { fun { <id> : <TYPE> } : <TYPE> <PICKY> }
          | { call <PICKY> <PICKY> }
          | { with { <id> : <TYPE> <PICKY> } <PICKY> }
          | { rec  { <id> : <TYPE> <PICKY> } <PICKY> }
          | { if <PICKY> <PICKY> <PICKY> }

<TYPE>  ::= Number
          | Boolean
          | ( <TYPE> -> <TYPE> )

Γ ⊢ n : Number

Γ ⊢ x : Γ(x)

Γ ⊢ A : Number  Γ ⊢ B : Number
———————————————————————————————
    Γ ⊢ {+ A B} : Number

Γ ⊢ A : Number  Γ ⊢ B : Number
———————————————————————————————
    Γ ⊢ {< A B} : Boolean

          Γ[x:=τ₁] ⊢ E : τ₂
——————————————————————————————————————
Γ ⊢ {fun {x : τ₁} : τ₂ E} : (τ₁ -> τ₂)

Γ ⊢ F : (τ₁ -> τ₂)  Γ ⊢ V : τ₁
——————————————————————————————
    Γ ⊢ {call F V} : τ₂

Γ ⊢ C : Boolean  Γ ⊢ T : τ  Γ ⊢ E : τ
———————————————————————————————————————
          Γ ⊢ {if C T E} : τ

Γ ⊢ V : τ₁  Γ[x:=τ₁] ⊢ E : τ₂
——————————————————————————————
Γ ⊢ {with {x : τ₁ V} E} : τ₂

Γ[x:=τ₁] ⊢ V : τ₁  Γ[x:=τ₁] ⊢ E : τ₂
—————————————————————————————————————
    Γ ⊢ {rec {x : τ₁ V} E} : τ₂

Typing Data

PLAI §27

An important concept that we have avoided so far is user-defined types. This issue exists in practically all languages, including the ones we did so far, since a language without the ability to create new user-defined types is a language with a major problem. (As a side note, we did talk about mimicking an object system using plain closures, but it turns out that this is insufficient as a replacement for true user-defined types — you can kind of see that in the Schlac language, where the lack of all types mean that there is no type error.)

In the context of a statically typed language, this issue is even more important. Specifically, we talked about typing recursive code, but we should also consider typing recursive data. For example, we will start with a length function in an extension of the language that has empty?, rest, and NumCons and NumEmpty constructors:

{rec {length : ???
      {fun {l : ???} : Number
        {if {empty? l}
          0
          {+ 1 {call length {rest l}}}}}}
  {call length {NumCons 1 {NumCons 2 {NumCons 3 {NumEmpty}}}}}}

But adding all of these new functions as built-ins is getting messy: we want our language to have a form for defining new kinds of data. In this example — we want to be able to define the NumList type for lists of numbers. We therefore extend the language with a new with-type form for creating new user-defined types, using variants in a similar way to our own course language:

{with-type {NumList [NumEmpty]
                    [NumCons Number ???]}
  {rec {length : ???
        {fun {l : ???} : Number
          ...}}
    ...}}

We assume here that the NumList definition provides us with a number of new built-ins — NumEmpty and NumCons constructors, and assume also a cases form that can be used to both test a value and access its components (with the constructors serving as patterns). This makes the code a little different than what we started with:

{with-type {NumList [NumEmpty]
                    [NumCons Number ???]}
  {rec {length : ???
        {fun {l : ???} : Number
          {cases l
            [{NumEmpty}    0]
            [{NumCons x r} {+ 1 {call length r}}]}}}
    {call length {NumCons 1 {NumCons 2 {NumCons 3 {NumEmpty}}}}}}}

The question is what should the ??? be filled with? Clearly, recursive data types are very common and we need to support them. The scope of with-type should therefore be similar to rec, except that it works at the type level: the new type is available for its own definition. This is the complete code now:

{with-type {NumList [NumEmpty]
                    [NumCons Number NumList]}
  {rec {length : (NumList -> Number)
        {fun {l : NumList} : Number
          {cases l
            [{NumEmpty}    0]
            [{NumCons x r} {+ 1 {call length r}}]}}}
    {call length {NumCons 1 {NumCons 2 {NumCons 3 {NumEmpty}}}}}}}

(Note that in the course language we can do just that, and in addition, the Rec type constructor can be used to make up recursive types.)

An important property that we would like this type to have is for it to be well founded: that we’d never get stuck in some kind of type-level infinite loop. To see that this holds in this example, note that some of the variants are self-referential (only NumCons here), but there is at least one that is not (NumEmpty) — if there wasn’t any simple variant, then we would have no way to construct instances of this type to begin with!

[As a side note, if the language has lazy semantics, we could use such types — for example:

{with-type {NumList [NumCons Number NumList]}
  {rec {ones : NumList {NumCons 1 ones}}
    ...}}

Reasoning about such programs requires more than just induction though.]