Solving the halting problem

Soon after teaching Turing machines, educators often explain why the halting
problem is undecidable. But then they seem to leave the story unfinished.
Have we just learned we can never trust software? How can we rely on a program
to control spacecraft or medical equipment if it can unpredictably loop
forever?

One might
claim extensive testing is the answer. We check a variety of cases work as
intended, then hope for the best. But though helpful, testing alone rarely
suffices. An untested case might occur naturally and cause bad behavour. Worse
still, a malicious user could scour the untested cases for ways to deliberately
sabotage our program.

The only true fix is to rein in those unruly Turing machines. Constraining our
code, might make it behave. We might try banning GOTO statements for example.

But how do we know if our restrictions are effective? Also, halting is but one
concern: even if we’re sure our program halts, it should still do the right
thing.

We’ll see that reasoning about our programs is easiest if we replace Turing
machines with an equally powerful model of computation: lambda calculus.
Only then can we introduce types, and draw on mathematics to prove correctness.

Unfortunately, some restrictions appear to have been invented without paying
any heed to theory. Could this be caused by the relative obscurity of lambda
calculus?

Simply typed lambda calculus

We can easily modify lambda calculus so that all programs halt while retaining
some power. We’ll walk through the solution that was first discovered, the
aptly named simply
typed lambda calculus.

Simply typed lambda calculus is also known as \(\lambda^{\rightarrow}\).

We start with base types, say Int and Bool, from which we
build other types with the (->)type constructor, such as:
Int -> (Int -> Bool). Conventionally, (->) is right associative, so we
write this as:

Int -> Int -> Bool

This type describes a function that takes an integer, and returns a function
mapping an integer to a boolean.

We can view this as a function that takes two Int parameters and returns
a single Bool. For example, the less-than function might have this type.
Representing functions that take multiple arguments as a series of functions
that each take one argument is known as currying.

We populate the base types with constants, such as 0, 1, … for Int,
and True and False for Bool.

This seems quotidian so far. Typical high-level languages do this sort
of thing. The fun part is seeing how easily it can be tacked on to lambda
calculus. There are only two changes:

We add a new kind of leaf node, which holds a constant.

The left child of a lambda abstraction (a variable) must be accompanied by
a type.

We only need to add a few lines to our lambda calculus example to add simple
types. Let’s get started:

First we need a new data structure to represent types. To avoid clashing
with predefined Haskell types, we use B for Bool and I for Int.

dataType=I|B|FunTypeTypederivingEq

By abuse of notation, Var will hold variables and constants: when the string
it holds is True, False, or a string representation of an integer, then it
counts as a constant. Otherwise it is a variable.

We add a type to the left child of a lambda abstraction.

We also add an If node, for if-then-else expressions. This has nothing to
do with simply typed lambda calculus, but we introduce it because, firstly,
we want to demonstrate how to extend lambda calculus, and secondly, simply
typed lambda calculus turns out to be a bit weak, and can use all the help
it can get!

Parsing

Because we must attach type signatures to variable bindings, apart from adding
a parser for types, we also change our lambda calculus parser so that (->) is
strictly a type constructor, and (.) is strictly for lambda abstractions.
Haskell gets away with using (->) for both cases because its grammar is
different. (For example, we can declare the type of a Haskell function
in one line, and define it in another.)

Typing

In a closed lambda term, every leaf node is typed because it’s either a
constant, or its type is given at its binding. Type checking works in
the obvious manner: for example, we can only apply a function of type
Int -> Int -> Bool to an Int, and we can only apply the resulting function
to an Int, and the result will be a Bool.

We predefine a few functions: negate, add, and not, whose types are
hard-coded here.

Traditionally, an uppercase gamma denotes a set of variables and their types,
which is called a typing context or typing environment, hence our use of
gamma for the first argument in our typeOf function:

Evaluation

Evaluation works the same as it does for untyped lambda calculus. In fact,
we could perform type erasure and drop the type of every bound variable
before evaluation: types are only needed during type checking. However,
keeping types around can be useful for sanity checks. (The GHC compiler has
a typed intermediate language for this reason.)

Otherwise, apart from handling predefined functions, our eval function
is the same as our corresponding function of untyped lambda calculus.

We assume the input is well-typed and hence closed. This simplifies the
function fv because let definitions are no longer permitted to contain free
variables.

User interface

We check a term is well-typed before adding it to our list of let definitions
or evaluating it. We print the type of terms to show off our new code.

For each let definition, we record the definition as well as its type.
Unlike our untyped lambda calculus interpreter, recursion is forbidden,
because we require the body of a let definition to be well-typed, and we
only prepopulate gamma with the types of previous let definitions.

With induction, we can show type checking is efficient, and if a closed lambda
term is well-typed, then it normalizes. (This implies the Y combinator and
omega combinator cannot be expressed in this system.) Moreover, any evaluation
strategy will lead to the normal form, that is, simply typed lambda calculus is
strongly normalizing.

In other words, programs always halt. Try doing this with Turing machines!

Simply typed, or typically simple?

Simply typed lambda calculus has limited power, but this is fine for certain
applications. In fact, it turns out some languages wind up embedding simply
typed lambda calculus in the type system. But for general-purpose programming,
we need more.

There are two ways to fix this:

We boldly add features like unrestricted recursion. We lose our guarantee
that all programs halt, but at least most of our language is trustworthy.

We cautiously enrich our type system, at each step checking that
well-typed programs normalize. With a sufficiently advanced type system,
we gain some forms of recursion.