My simply typed lambda calculus interpreter

What's the best way to start writing an interpreter for the simply typed lambda calculus? Well, by writing an interpreter for the untyped lambda calculus. The untyped lambda calculus is a good target to compile to after you've finished typechecking a simply typed program, and simple types would be ignored at execution time anyway.

Below is the untyped interpreter, in Scala, that I used as my target for this exercise. It's a bit different from the Scala interpreter I wrote for the previous exercise on the untyped lambda calculus:

To to the basic lambda calculus Var/Lam/App constructs, I've added Con (for constants of types Unit, Boolean, and Int) and If (baking in booleans and conditionals gives them a more familiar look than defining them as functions, which you're forced to do in the unaugmented calculus). Runtime values include not only Unit, Boolean, and Int constants, but also function closures (Clo) and built-in functions (Fun) that manipulate the three supported base types.

I've tightened up my syntactic sugar for my internal DSL representation of the lambda calculus. There are implicit conversions from everything you're likely to want to put into a lambda calculus expression, and conversion of a term to a String emits fewer redundant parentheses. The intention is that if you write a lambda calculus DSL expression as clearly as possible in the Scala source, and then call toString on that, you should get back the same Scala source text.

For the previous exercise, I wrote interpreters in two different styles—structural reduction by rewriting the lambda calculus term, and a closure-based tail-recursive interpreter in SECD style. Here I have a third style of interpreter, a naive (non-tail-recursive) closure-based interpreter of the type you're likely to write in a beginning course on programming languages. This is probably the most familiar and readable type of interpreter, but the lack of tail recursion means that it's likely to overflow the Scala stack if you try to make it do a nontrivial amount of work.

Here is the code that compiles the simply typed lambda calculus for the untyped interpreter of my previous message.

The main function that actually does something here is compile, which performs typechecking while doing a translation to the untyped lambda calculus—the run function just compiles and then invokes the untyped interpreter.

The structure of the simply typed lambda calculus is largely parallel to that of the untyped. I could perhaps have tried to factor out the differences between the corresponding Term/Type/Value/Env/Var/Lam/App/Con/If classes, but there are enough minor differences between the two calculi that I doubt the result would have been more readable than just writing corresponding pairs of not-quite-identical classes.

I've introduced a primitive Fix term to permit recursion, which cannot otherwise be given a correct simple type. The corresponding construct in the untyped implementation is just a function fix that applies a fixpoint combinator, since recursion can be expressed directly in the untyped calculus. Fix (sometimes known as the mu operator) has a structure parallel to Lam, and I've factored out their commonalities into a superclass Parmed.

Compilation conceptually just verifies that the declared simple types match up, and translate the elements of the simply typed calculus to their untyped equivalents. If compilation fails, compile produces a list of String error messages. The place where the App case checks whether funArgT == argT is most characteristic of simple types: function parameter and argument types must match exactly. A more complicated type system would have to more work here—for example, an implementation of parametric polymorphism might need to unify different type variables.