Lecture 22 (2018-04-05)
Types for the lambda calculus
> import qualified Data.Map as Map
> import Data.Map (Map)
We discussed type systems, first for a version of the While language
with variables that could be either bools or ints, and then for the
lambda calculus.
Broadly, we used type systems to identify programs that will "go
wrong" when we run them. For example, in the While language we wanted
to avoid trying to add a number to a boolean or to condition a while
loop on a number. In the lambda calculus, we wanted to avoid applying
a non-function or conditioning on a non-boolean.
We can to express our type systems in two ways: first, as a relation
defined by inference rules; second, as a function that tried to assign
types to terms.
### Typing relation for the lambda calculus with booleans

First, let's extend the lambda calculus to include booleans. Note that
we're forcing programmers to write the argument type on lambdas---it's
a convenience for our own design.
```
t ::= bool | t1 -> t2
e ::= x | e1 e2 | lambda x:t. e | true | false | if e1 e2 e3
```
Or, in Haskell:
> data Type = TBool | TFun Type Type deriving (Eq, Show)
>
> type VarName = String
> data Expr =
> Var VarName
> | App Expr Expr
> | Lam VarName Type Expr
> | Bool Bool
> | If Expr Expr Expr deriving (Eq, Show)
We can define a step semantics for this calculus as follows:
```
lambda x:t. e is a value
true is a value
false is a value
v2 is a value
--------------------------------
(lambda x:t. e1) v2 --> e1[v2/x]
e1 --> e1'
----------------
e1 e2 --> e1' e2
v1 is a value e2 --> e2'
---------------------------
v1 e2 --> v1 e2'
--------------------
if true e2 e3 --> e2
---------------------
if false e2 e3 --> e3
e1 --> e1'
----------------------------
if e1 e2 e3 --> if e1' e2 e3
```
Note that many terms are *stuck*, i.e., unable to take a step: `true`
doesn't take any steps, nor do lambdas; other terms like `true (lambda
x:bool. x)` or `if (lambda y:bool. y) false true` are also
stuck. Having values be stuck is just dandy---values are done
evaluating, so there shouldn't be any steps to take. We have some
non-values that are stuck, though, and that's what we'll use to define
"wrongness".
Let's define a type system that rules out wrongness. First, we need a
way to keep track of the types of variables; we'll use a *context*
`Γ`, which is the Greek letter "capital gamma". We'll write `Γ |- e :
t` to say that the term `e` has type `t` under context `Γ`. Note that
`|-` (pronounced "turnstile" or "shows") and `:` (pronounced "colon"
or "has type") are just arbitrary syntax.
```
Γ ::= empty | Γ;x:t
lookup(Γ;x:t, x) = t
lookup(Γ;y:t, x) = lookup(Γ, x)
lookup(Γ,x) = t
---------------
Γ |- x : t
Γ |- e1 : t1 -> t2 Γ |- e2 : t1
----------------------------------
Γ |- e1 e2 : t2
Γ;x:t1 |- e : t2
------------------------------
Γ |- lambda x:t1. e : t1 -> t2
----------------
Γ |- true : bool
-----------------
Γ |- false : bool
Γ |- e1 : bool Γ |- e2 : t Γ |- e3 : t
--------------------------------------------
Γ |- if e1 e2 e3 : t
```
Notice how we constrain both branches of an if to have the same type;
how the rule for lambda extends the context to keep track of the
argument type; and how the application rule makes sure we (a) only
apply functions and (b) give suitable arguments to those functions.
Try to construct a derivation of the fact that `Γ |- lambda x:bool. x : bool -> bool`
and `Γ |- if true false true : bool`. Convince
yourself that `true (lambda x:bool. x)` or `if (lambda y:bool. y)
false true`---our non-value stuck terms above---can't be well typed.
### Typing functions

The well typing relation `Γ |- e : t` is hopefully a clear way to
understand, declaratively, which terms are well typed and which
aren't. But Haskell doesn't have "relations"---it has functions. Let's
write a function that determines whether or not a given program is
well typed.
The first question we must ask is: what type should the function have?
In the relation `Γ |- e : t`, we can consider `Γ` and `e` as inputs
and `t` as an output: you give me a context and a term, I'll give you
a type. So as a first cut:
```haskell
type Context = Map VarName Type
typeOf :: Context -> Expr -> Type
```
But what should we do when a term *isn't* well typed? We could throw
an error, but that's pretty annoying and makes it hard to use the
`typeOf` function in other code. Instead, it's better to make errors
explicit:
> type Context = Map VarName Type
>
> data TypeError = ExpectedFunction Expr Type
> | Mismatch Expr Type Type {- expression, got, expected -}
> | UnboundVariable VarName deriving Show
>
> typeOf :: Context -> Expr -> Either TypeError Type
> typeOf g (Var x) =
> case Map.lookup x g of
> Nothing -> Left $ UnboundVariable x
> Just t -> pure t
> typeOf g (Lam x t1 e) = do
> t2 <- typeOf (Map.insert x t1 g) e
> pure $ TFun t1 t2
> typeOf g e@(App e1 e2) = do
> t1 <- typeOf g e1
> t2 <- typeOf g e2
> case t1 of
> TFun t11 t12 | t11 == t2 -> pure t12
> TFun t11 t12 -> Left $ Mismatch e t2 t11
> _ -> Left $ ExpectedFunction e1 t1
> typeOf _ (Bool _) = pure TBool
> typeOf g (If e1 e2 e3) = do
> t1 <- typeOf g e1
> t2 <- typeOf g e2
> t3 <- typeOf g e3
> case t1 of
> TBool | t2 == t3 -> pure t2
> TBool -> Left $ Mismatch e3 {- arbitrary! -} t3 t2
> _ -> Left $ Mismatch e1 t1 TBool
Note how we use `do` notation to automatically thread errors in the
Either monad. Note also how each case of our recursive definition
corresponds exactly to one of the rules in our relation. We have such
a neat coincidence because our typing relation is *syntax directed*;
that is, each syntax node can be typed by exactly one rule.