Preface

These are lecture notes for the "Programming Languages I" course at the University of Tübingen. These lecture notes have at some point started as a Scala version of a subset of the excellent book "Programming Languages: Application and Interpretation" by Shriram Krishnamurthi, but over the years they have been modified and extended considerably.

These lecture notes can be improved by your corrections, comments and amendments. Every page of these lectures notes comes with a link to the corresponding document hosted on Github in the top right corner. Please open a pull request for any changes that help us improve this script.

Klaus Ostermann

Introduction

Why are you here?

This is an advanced programming course that focuses on the design and semantics of programming languages. The goals of this course are:

  • To change the way you solve problems in programming: By principled design, with a rich toolbox of abstract concepts, rather than by tinkering.
  • Enable you to think beyond the currently fashionable programming languages.
  • Enable you to evaluate and discuss programming languages in a principled way.
  • To show you where computer languages come from and potential future directions.
  • To demonstrate why languages should be regarded as the ultimate form of abstraction.
  • To teach you how to recognize abstractions and how to turn them into a language design.
  • Teach you different ways how to realize a language design and make it practically useful.
  • To convey a sense of aesthetics in programming.
  • To introduce you into one of the most beautiful theories in theoretical computer science.
  • To turn you into a better programmer!

Non-goals of this course are:

  • Learn X different programming languages. Rather, you'll learn to decompose a PL into its features and discuss the merits of the features independently of the concrete PL.
  • Write parsers or implement full-fledged compilers (although we will touch some compiler issues).
  • Turn you into an evangelist for a particular PL.

How will we get there?

We will typically explain language features in two steps. First, we'll give an informal introduction and discuss some examples to get some experience with it. Then we will distill the core of the feature into an executable interpreter. Later, when we have more experience in formalizing language design into interpreters, we will also use formal mathematical notation. We will not just present the design of these features as if it had appeared out of the blue sky. Rather, we will discuss the process and trade-offs (and potential mistakes and pitfalls) that led to these designs. We will also discuss how these features appear in various available programming languages. We will mainly use the programming language Scala to write interpreters. Scala is sufficiently powerful to allow concise and elegant interpreters; on the other hand, it is sufficiently mature and popular for industrial usage. It also has variants of many of the features we will discuss. The book by Krishnamurthi, on which we loosely base the first part of the course, uses a different language, Racket. You are also welcome to use Racket instead of (or in addition to) Scala. Racket embodies an important tradition of PL design that is worth knowing, hence we encourage you, independently of this course, to study it!

How we will study programming languages

Rather than use vague labels such as "object-oriented" or "functional", we will decompose languages into small features, similar to how matter can be explained and decomposed into atoms, cells into DNA fragments, or organisms into the biological taxonomy. In addition to the decomposition into small-grained features, we will use a conceptual technique called "desugaring" to distinguish essential from non-essential language parts and identify the core constructs of a language.

Topics for class discussion:

  • How can programming languages be evaluated, classified and compared?
  • Why are labels like "object-oriented", "declarative", or "functional" too coarse-grained to be useful?
  • How can we turn programming language research into a science?
  • What would you like to learn in this course? What are your expectations?
  • Which programming languages do you know? Which features of these languages do you like or dislike, and why?
  • Is the choice of programming language for a programming project significant?
  • Are interpreters a good way to capture the meaning of a PL? How do they compare to informal descriptions, formal specifications, and compilers?
  • How important is the syntax of a PL?

Scala Basics

The content of this chapter is available as a Scala file here.

val x = 2 + 2;
// x: Int = 4
// val für Konstanten
// def für Methoden
// var für Variablen

abstract class Person(val name: String) {
  // String name
  // val name: String

  def sayHello = {
    print("hello, " + name)
  }
}

trait ByeBye {
  def sayGoodbye = {
    print("bye")
  }
}

class Tillmann(name: String) extends Person(name) with ByeBye {
  override def sayHello = {
    print("hi")
  }
}

object Me extends Tillmann("Tillmann")




trait UniPerson
case class Student(val matrikelnummer: Int) extends UniPerson
case class Professor(val fachgebiet: String) extends UniPerson

object PatternMatching {
  def showUniPerson(p: UniPerson): String =
    p match {
      case Student(m) => "student nr " + m
      case Professor(f) => "professor on " + f
    }

  def test = {
    print(showUniPerson(Student(123)))
    print(showUniPerson(Professor("programming languages")))
  }
}


enum UniPersonEnum:
  case StudentEnum(val matrikelnummer: Int)
  case ProfessorEnum(val fachgebiet: String)

object PatternMatchingEnum {
  import UniPersonEnum._

  def showUniPerson(p: UniPersonEnum): String =
    p match {
      case StudentEnum(m) => "student nr " + m
      case ProfessorEnum(f) => "professor on " + f
    }

  def test = {
    print(showUniPerson(StudentEnum(123)))
    print(showUniPerson(ProfessorEnum("programming languages")))
  }
}


object AE {
  // Abstract Syntax Tree
  enum Exp:
    case Num(n: Int)
    case Add(lhs: Exp, rhs: Exp)

  import Exp._

  // Example
  val onePlusEight = Add(Num(1), Add(Num(5), Num(3)))

  // Interpreter
  def eval(e: Exp): Int =
    e match {
      case Num(n) => n
      case Add(lhs, rhs) =>
        eval(lhs) + eval(rhs)
    }
}

Arithmetic Expressions With Variables

The content of this chapter is available as a Scala file here.

Let us now consider an extension of the arithmetic expression language with variables. We do this by a new kind of expression, which we call Identifier, or Id.

\[ \begin{array}{lclr} e & := & n & \textit{Numeric Literals} \\ & | & (e + e) & \textit{Addition} \\ & | & (e * e) & \textit{Multiplication} \\ & | & x & \textit{Identifier} \end{array} \]

object AEId {
enum Exp:
  case Num(n: Int) extends Exp
  case Add(lhs: Exp, rhs: Exp) extends Exp
  case Mul(lhs: Exp, rhs: Exp) extends Exp
  case Id(x: String) extends Exp
import Exp._

Here is a sample program written in this language, directly written down using constructors of the enum:

val test0 = Add(Mul(Id("x"), Num(2)), Add(Id("y"), Id("y")))

With a proper parser we could choose a syntax like x * 2 + y + y. We do not care much about concrete syntax and parsing, though.

That said, to make writing examples less verbose, Scala's implicits come to the rescue. Calls to implicit functions are inserted automatically by the compiler if they help to restore well-typedness. For instance, we can define

implicit def num2exp(n: Int): Exp = Num(n)
implicit def sym2exp(x: String): Exp = Id(x)

to lift integers and strings to expressions. Using these implicits, the example can be written as:

val test = Add(Mul("x", 2), Add("y", "y"))

To give meaning to identifiers, we use environments. Environments are mappings from identifiers (which we represent as strings) to values. In our simple language the only values are integers, hence:

type Env = Map[String, Int]

An evaluator (or interpreter) for this language takes an expression and an environment as parameter and produces a value - in this case Int. This interpreter uses pattern matching over the constructors of enum Exp.

def eval(e: Exp, env: Env): Int = e match {
  case Num(n) => n
  case Id(x) => env(x)
  case Add(l, r) => eval(l, env) + eval(r, env)
  case Mul(l, r) => eval(l, env) * eval(r, env)
}

A different (and arguably more 'object-oriented') way to implement this evaluator would be to add an abstract eval method to an Exp class and override it in all subclasses, each implementation corresponding to its corresponding case in the pattern match. The choice between these alternatives matters, since they support different dimensions of extensibility.

We will mainly use the more functional style using pattern matching, because it matches better to the order in which we present these topics in the lecture. To try the example, we need a sample environment that gives values to the (free) variables in the sample expression.

The test environment also illustrates how Scala supports direct definitions of constant maps.

val testEnv = Map("x" -> 3, "y" -> 4)

We can automatically test our evaluator using assert:

val exa = eval(test, testEnv)
// exa: Int = 14
assert(eval(test, testEnv) == 14)
}

Internal Visitors

We will now learn a different way to encode algorithms that operate on expressions (like the evaluator). To this end, we will now use so-called "folds". Folds are well-known for lists, but the concept is more general and applies to arbitrary algebraic data types. We will present folds in such a style that they resemble visitors as known from the OO design pattern literature. They correspond to so-called "internal visitors" in which the traversal is encoded within the "accept" function. An internal visitor consists of one function for each syntactic construct of the language. It has a type parameter that determines the "return type" of invoking the visitor on an expression. This type parameter is used in all positions in which the original syntax specifies a subexpression. Internal visitors also correspond to a "bottom-up" traversal of the syntax tree.

object Visitors {
case class VisitorAE[T](num: Int => T, add: (T, T) => T)

The fold function below itself applies a visitor to an expression. Note that the recursion is performed in the fold function, hence all visitors are not recursive.

Also note that this design enforces that all algorithms specified via this visitor interface are compositional by design. This means that the recursion structure of the algorithm corresponds to the recursion structure of the expression. Put in another way, it means that the semantics (in terms of the meta-language) of a composite expression is determined by the semantics of the subexpressions; the syntax of the subexpressions is irrelevant.

Compositional specifications are particularly nice because they enable "equational reasoning": Subexpressions can be replaced by other subexpressions with the same semantics without changing the semantics of the whole.

enum ExpAE:
  case NumAE(n: Int) extends ExpAE
  case AddAE(lhs: ExpAE, rhs: ExpAE) extends ExpAE
import ExpAE._

def foldExp[T](v: VisitorAE[T], e: ExpAE): T = {
  e match {
    case NumAE(n) => v.num(n)
    case AddAE(l, r) => v.add(foldExp(v, l), foldExp(v, r))
  }
}

Here is our evaluator from above rephrased using the visitor infrastructure.

val evalVisitorAE = VisitorAE[Int](x => x, (a, b) => a + b)

We can of course restore the original interface of eval using foldExp:

def eval(e: ExpAE) = foldExp(evalVisitorAE, e)

Let's test whether it works.

val exaVisitorAE = eval(AddAE(AddAE(NumAE(1), NumAE(2)), NumAE(3)))
// exaVisitorAE: Int = 6
assert(exaVisitorAE == 6)

We can also apply other algorithms using visitors, such as counting the number of NumAE literals, or printing to a string:

val countVisitorAE = VisitorAE[Int](_ => 1, _ + _)
val printVisitorAE = VisitorAE[String](_.toString, "(" + _ + "+" + _ + ")")
}

Let's now try the same with the AE language with identifiers. It all works in the same way:

object AEIdVisitor {
import AEId._
case class Visitor[T](num: Int => T, add: (T, T) => T, mul: (T, T) => T, id: String => T)
val expVisitor = Visitor[Exp](Num(_), Add(_, _), Mul(_, _), Id(_))
val countVisitor = Visitor[Int](_ => 1, _ + _, _ + _, _ => 0)
val printVisitor = Visitor[String](_.toString, "(" + _ + "+" + _ + ")", _ + "*" + _, identity)

def foldExp[T](v: Visitor[T], e: Exp): T = {
  e match {
    case Num(n) => v.num(n)
    case Add(l, r) => v.add(foldExp(v, l), foldExp(v, r))
    case Mul(l, r) => v.mul(foldExp(v, l), foldExp(v, r))
    case Id(x) => v.id(x)
  }
}
def countNums(e: Exp) = foldExp(countVisitor, e)

val exaCount = countNums(test)
// exaCount: Int = 1
assert(exaCount == 1)

However, what about the evaluator? If we instantiate T = Int, then how can we access the environment? Insight: For evaluation, we must instantiate T with a function type Env => Int! This way of transforming a multi-argument function into a single-argument higher-order function is called currying.

val evalVisitor = Visitor[Env => Int](
   env => _ ,
   (a, b) => env =>
     a(env) + b(env),
   (a, b) => env =>
     a(env) * b(env),
   x => env =>
     env(x))
}

Desugaring

The content of this chapter is available as a Scala file here.

Desugaring

You have already seen the basic structure of an interpreter by means of an interpreter for a language of arithmetic Exps:

object AE {
  // Abstract Syntax Tree
  enum Exp:
    case Num(n: Int) extends Exp
    case Add(lhs: Exp, rhs: Exp) extends Exp
  import Exp._

  // Example
  val ex = Add(Num(1), Add(Num(5), Num(3)))

  // Interpreter
  def eval(e: Exp): Int =
    e match {
      case Num(n) => n
      case Add(lhs, rhs) =>
        eval(lhs) + eval(rhs)
    }
}

In this lecture we want to study the technique of desugaring as a means to structure programming languages and decompose a language into a core language and syntactic sugar.

For illustration, consider the following proposed extensions to the language:

  1. Mult
  2. Sub
  3. Unary Negation

Extension number 1 is a good example for a core language extension. We have no way of expressing Mult in terms of the existing constructs (if we had some looping construct we could express Mult as repeated Add but we do not have loops).

Hence we add this language construct to the (core) language:

object MAE {
  // Abstract Syntax Tree
  enum Exp:
    case Num(n: Int) extends Exp
    case Add(lhs: Exp, rhs: Exp) extends Exp
    case Mult(lhs: Exp, rhs: Exp) extends Exp
  import Exp._
  // Example
  val ex = Add(Num(1), Mult(Num(5), Num(3)))

  // Interpreter
  def eval(e: Exp): Int =
    e match {
      case Num(n) => n
      case Add(lhs, rhs) => eval(lhs) + eval(rhs)
      case Mult(lhs, rhs) =>  eval(lhs) * eval(rhs)
    }
}

Let us now consider extension #2, Sub. One way to support sub is to add it to the core language, just like Mult:

object SMAE {
  // Abstract Syntax Tree
  enum Exp:
    case Num(n: Int) extends Exp
    case Add(lhs: Exp, rhs: Exp) extends Exp
    case Mult(lhs: Exp, rhs: Exp) extends Exp
    case Sub(lhs: Exp, rhs: Exp) extends Exp
  import Exp._
  // Example
  val ex = Sub(Num(1), Mult(Num(5), Num(3)))

  // Interpreter
  def eval(e: Exp): Int =
    e match {
      case Num(n) => n
      case Add(lhs, rhs) => eval(lhs) + eval(rhs)
      case Mult(lhs, rhs) =>  eval(lhs) * eval(rhs)
      case Sub(lhs, rhs) =>  eval(lhs) - eval(rhs)
    }
}

However, another way of adding sub is to treat it as syntactic sugar using the fact that a - b = a + (-1 * b) One way of expressing the desugaring is as a syntax transformation:

def desugarSMAE2MAE(e: SMAE.Exp): MAE.Exp = e match {
  case SMAE.Exp.Num(n) => MAE.Exp.Num(n)
  case SMAE.Exp.Add(lhs, rhs) => MAE.Exp.Add(desugarSMAE2MAE(lhs), desugarSMAE2MAE(rhs))
  case SMAE.Exp.Mult(lhs, rhs) => MAE.Exp.Mult(desugarSMAE2MAE(lhs), desugarSMAE2MAE(rhs))
  case SMAE.Exp.Sub(lhs, rhs) =>
    MAE.Exp.Add(desugarSMAE2MAE(lhs),
                 MAE.Exp.Mult(MAE.Exp.Num(-1), desugarSMAE2MAE(rhs)))
}

With this desugaring in place, we do not need an interpreter for SMAE anymore; rather we can reuse the MAE interpreter:

val res = MAE.eval(desugarSMAE2MAE(SMAE.ex))
// res: Int = -14

If we had written other algorithms on MAE, or had proven properties of MAE, they'd be applicable to SMAE, too. Hence desugaring is a way of reusing code, proofs, ... . It is important, though, that the desugared language feature is gone after desugaring. For instance, a pretty printer would print the desugared code. A debugger would use the desugared code. This can be an important downside to desugaring. There are ways to avoid or mitigate these shortcomings, but they require additional work.

There is a second way of realizing desugaring which does not require the definition of a copy of the AST enums. We can desugar earlier, namely during the construction of the AST:

object SMAE2 {
  // Abstract Syntax Tree
  enum Exp:
    case Num(n: Int) extends Exp
    case Add(lhs: Exp, rhs: Exp) extends Exp
    case Mult(lhs: Exp, rhs: Exp) extends Exp
  object Exp:
    def sub(e1: Exp, e2: Exp): Exp =
      Add(e1, Mult(Num(-1), e2))
  import Exp._

  // Compared to SMAE, we only have to change upper case Sub by lower case sub
  // when constructing examples.
  val ex = sub(Num(1), Mult(Num(5), Num(3)))

  // Interpreter - no case for sub needed
  def eval(e: Exp): Int =
    e match {
      case Num(n) => n
      case Add(lhs, rhs) => eval(lhs) + eval(rhs)
      case Mult(lhs, rhs) =>  eval(lhs) * eval(rhs)
    }
}

Let us now consider the third extension, unary minus. Here we have three choices:

  1. Add unary minus to the core language.
  2. Treat unary minus as syntactic sugar for the core language using -x = (-1) * x.
  3. Treat unary minus as syntactic sugar on top of the syntactic sugar using -x = 0 - x.

We will use the third option to illustrate that one can build layers of syntactic sugar:

object USMAE {
  // Abstract Syntax Tree
  enum Exp:
    case Num(n: Int) extends Exp
    case Add(lhs: Exp, rhs: Exp) extends Exp
    case Mult(lhs: Exp, rhs: Exp) extends Exp
  object Exp:
    def sub(e1: Exp, e2: Exp): Exp =
      Add(e1, Mult(Num(-1), e2))
    def unaryminus(e: Exp) = sub(Num(0), e)
  import Exp._

  // Compared to SMAE, we only have to change upper case Sub by lower case sub
  // when constructing examples.
  val ex = sub(unaryminus(Num(1)), Mult(Num(5), Num(3)))

  // Interpreter - no case for sub needed
  def eval(e: Exp): Int =
    e match {
      case Num(n) => n
      case Add(lhs, rhs) => eval(lhs) + eval(rhs)
      case Mult(lhs, rhs) =>  eval(lhs) * eval(rhs)
    }
}

Enable JavaScript to see the quiz

What is the result of desugaring the following expression in the language USMAE?

  sub(unaryminus(Num(1)), Num(7))
    

  sub(unaryminus(Num(1)), Num(7))
      
No desugaring was applied.

  Num(-8)
      
Desugaring does not mean evaluation.

  sub(sub(Num(0), Num(1)), Num(7))
      
sub has to be desugared, too.

  Add(Add(Num(0), Mult(Num(-1), Num(1))),
      Mult(Num(-1), Num(7)))
      

Name Binding

The content of this chapter is available as a Scala file here.

import scala.language.implicitConversions

Name Binding

We want, step by step, to develop our primitive calculator language into a full-fledged PL. One important milestone on this way is the ability to deal with names. While our previous language allowed expressions with identifiers in them, it had no binders: Constructs that allow to give meaning to a new name. In this variant of the language, called WAE, we introduce such a binder called "with" with which we can give an expression a name that can be used in the body of the "with" expression. This intuition is captured in the definition of the With case below, which extends our previous language. We study this WAE language to better understand what names mean in programming languages, and how they can be implemented.

object Syntax {
  enum Exp:
    case Num(n: Int) extends Exp
    case Add(lhs: Exp, rhs: Exp) extends Exp
    case Mul(lhs: Exp, rhs: Exp) extends Exp
    case Id(x: String) extends Exp
    case With(x: String, xdef: Exp, body: Exp) extends Exp
}
import Syntax._
import Exp._

We use implicits again to make example programs less verbose.

implicit def num2exp(n: Int): Exp = Num(n)
implicit def string2exp(x: String): Exp = Id(x)

A first example program in WAE.

val test = With("x", 5, Add("x", "x"))

Note that we deal with two languages here:

  1. This Scala file with Scala code.
  2. Most of the functions work on programs written in the WAE language.

Most of the time, we concentrate on WAE, but sometimes, we also talk about Scala. We have not defined a concrete syntax for WAE, but it is a real language nevertheless. We sometimes use some made-up syntax for examples on the blackboard or in comments.

Substitution

Instead of dealing with identifiers as external entities as in AE, identifiers can now be defined within the language. This justifies a new treatment of identifiers. We will explain them in terms of substitution, a notion well-known informally from high school algebra. The idea is the following: The interpreter transforms the term

  with (x = 5) {
    x + x
  }

into

  5 + 5

before proceeding. That is, all occurrences of x have been replaced by 5. Note that these two programs -- before and after the substitution -- are certainly not equal: They look quite different. However, they are equivalent in the sense that when evaluated, they will produce the same number. Such transformations between different but somehow equivalent programs are an important tool for the study of programs, and of programming languages. Often, if we know which programs behave identically, we understand better how programs behave in general. We will see more examples of this in this lecture. Hence, the implementation of the With-case of our interpreter should be something like:

case With(x, xdef, body) => eval(subst(body, x, Num(eval(xdef))))

for a function subst with signature

subst: (Exp, String, Num) => Exp

The type of the third parameter is Num instead of Exp because it is more difficult to get substitution correct when arbitrary expressions can be inserted (accidential name capture problem, more about that later). Since we want to experiment with different versions of substitution, we write the interpreter in such a way that we can parameterize it with a substitution function:

def makeEval(subst: (Exp, String, Num) => Exp): Exp => Int = {
  def eval(e: Exp): Int = e match {
    case Num(n) => n
    case Id(x) => sys.error("unbound variable: " + x)
    case Add(l, r) => eval(l) + eval(r)
    case Mul(l, r) => eval(l) * eval(r)
    // take the Int and wrap it into a Num for substitution
    case With(x, xdef, body) => eval(subst(body, x, Num(eval(xdef))))
  }
  eval
}

Substitution, take 1

To substitute identifier i in e with expression v, replace all identifiers in e that have the name i with the expression v. Let's try to formalize this definition:

val subst1: (Exp, String, Num) => Exp = (e, i, v) => e match {
  case Num(n) => e
  case Id(x) => if (x == i) v else e
  case Add(l, r) => Add(subst1(l, i, v), subst1(r, i, v))
  case Mul(l, r) => Mul(subst1(l, i, v), subst1(r, i, v))
  case With(x, xdef, body) => With(if (x == i) v else x,
                                    subst1(xdef, i, v),
                                    subst1(body, i, v))
  }

Unfortunately this does not even type-check! And rightly so, because it might otherwise turn reasonable programs into programs that are not even syntactically legal anymore. Exercise for self-study: Find an expression that would be transformed into one that is not syntactically legal. To see the reason for this, we need to define some terminology (the word "instance" here means "occurence"):

Definition (Binding Instance): A binding instance of an identifier is the instance of the identifier that gives it its value. In WAE, the x position of a With is the only binding instance.

Definition (Scope): The scope of a binding instance is the region of program text in which instances of the identifier refer to the value bound by the binding instance.

Definition (Bound Instance): An identifier is bound if it is contained within the scope of a binding instance of its name.

Definition (Free Instance): An identifier not contained in the scope of any binding instance of its name is said to be free.

Examples: In WAE, the String in Id("x") is a bound or free instance, and the String in With("x", ..., ...) is a binding instance. The scope of this binding instance is the third sub-term of With.

Now the reason can be revealed. Our first attempt failed because we substituted the identifier occurring in the binding position in the With-expression. This renders the expression illegal because after substitution the binding position where an identifier was expected is now occupied by a Num. To correct this mistake, we make another take at substitution:

Substitution, take 2

To substitute identifier i in e with expression v, replace all identifiers in e which are not binding instances that have the name i with the expression v. Here is the formalization of this definition.

val subst2: (Exp, String, Num) => Exp = (e, i, v) => e match {
  case Num(n) => e

  // Bound or free instance => substitute if names match
  case Id(x) => if (x == i) v else e

  case Add(l, r) => Add(subst2(l, i, v), subst2(r, i, v))
  case Mul(l, r) => Mul(subst2(l, i, v), subst2(r, i, v))

  // binding instance => do not substitute
  case With(x, xdef, body) => With(x,
                                  subst2(xdef, i, v),
                                  subst2(body, i, v))
}

Let's create an interpreter that uses this substitution function.

def eval2 = makeEval(subst2)

assert(eval2(test) == 10) // it works!

val test2 = With("x", 5, Add("x", With("x", 3, 10))) // another test

assert(eval2(test2) == 15) // works as expected

val test3 = With("x", 5, Add("x", With("x", 3, "x"))) // another test

// assert(eval2(test3) == 8) // Bang! Result is 10 instead!

What went wrong here? Our substitution algorithm respected binding instances, but not their scope. In the sample expression, the With introduces a new scope for the inner x. The scope of the outer x is shadowed or masked by the inner binding. Because substitution doesn’t recognize this possibility, it incorrectly substitutes the inner x.

Substitution, take 3

To substitute identifier i in e with expression v, replace all non-binding identifiers in e having the name i with the expression v, unless the identifier is in a scope different from that introduced by i.

val subst3: (Exp, String, Num) => Exp = (e, i, v) => e match {
    case Num(n) => e
    case Id(x) => if (x == i) v else e
    case Add(l, r) => Add(subst3(l, i, v), subst3(r, i, v))
    case Mul(l, r) => Mul(subst3(l, i, v), subst3(r, i, v))
    case With(x, xdef, body) => With(x,
                                    subst3(xdef, i, v),
                                    // what if we forget to substitute into the body?
                                    body)
}

def eval3 = makeEval(subst3)

assert(eval3(test) == 10)

assert(eval3(test2) == 15)

assert(eval3(test3) == 8) // Success!

val test4 = With("x", 5, Add("x", With("y", 3, "x")))

// assert(eval3(test4) == 10) // Bang! unbound variable: "x"

The inner expression should result in an error, because x has no value. Once again, substitution has changed a correct program into an incorrect one! Let’s understand what went wrong. Why didn’t we substitute the inner x? Substitution halts at the With because, by definition, every With introduces a new scope, which we said should delimit substitution. But this With contains an instance of x, which we very much want substituted! So which is it - substitute within nested scopes or not? Actually, the two examples above should reveal that our latest definition for substitution, which may have seemed sensible at first blush, is too draconian: it rules out substitution within any nested scopes.

Substitution, take 4

To substitute identifier i in e with expression v, replace all non-binding identifiers in e having the name i with the expression v, except within nested scopes of i. Finally, we have a version of substitution that works. A different, more succinct way of phrasing this definition is: "To substitute identifier i in e with expression v, replace all free instances of i in e with v."

val subst4: (Exp, String, Num) => Exp = (e, i, v) => e match {
    case Num(n) => e
    case Id(x) => if (x == i) v else e
    case Add(l, r) => Add(subst4(l, i, v), subst4(r, i, v))
    case Mul(l, r) => Mul(subst4(l, i, v), subst4(r, i, v))
    // do not substitute when shadowed
    case With(x, xdef, body) => if (x == i) e
                                 else With(x,
                                           subst4(xdef, i, v),
                                           subst4(body, i, v))
}

def eval4 = makeEval(subst4)

assert(eval4(test) == 10)

assert(eval4(test2) == 15)

assert(eval4(test3) == 8)

assert(eval4(test4) == 10) // Success!

val test5 = With("x", 5, With("x", "x", "x"))

// assert(eval4(test5) == 5) // Bang! unbound variable "x"

This program should evaluate to 5, but it too halts with an error. This is because we prematurely stopped substituting for x occuring in a bound position. We should substitute in the named expression of a With even if the With in question defines a new scope for the identifier being substituted, because its named expression is still in the scope of the enclosing binding of the identifier.

Substitution, take 5

We finally get a valid programmatic definition of substitution (relative to the language we have so far):

val subst5: (Exp, String, Num) => Exp = (e, i, v) => e match {
    case Num(n) => e
    case Id(x) => if (x == i) v else e
    case Add(l, r) => Add(subst5(l, i, v), subst5(r, i, v))
    case Mul(l, r) => Mul(subst5(l, i, v), subst5(r, i, v))
    // handle shadowing correctly
    case With(x, xdef, body) => With(x,
                                   subst5(xdef, i, v),
                                   if (x == i) body else subst5(body, i, v))
}

def eval5 = makeEval(subst5)

assert(eval5(test) == 10)

assert(eval5(test2) == 15)

assert(eval5(test3) == 8)

assert(eval5(test4) == 10)

assert(eval5(test5) == 5) // Success!

Summary

  1. Substitution can be used to understand the meaning of names in programming languages.
  2. Correct implementations of substitution need to handle free, bound, and binding instances of names and their scopes correctly.

First-Order Functions

The content of this chapter is available as a Scala file here.

First-Order Functions

In the last lecture we have seen how we can give commonly occuring (sub)expressions a name via the With construct. Often, however, we can identify patterns of expressions that occur in many places, such as 5*5/2, 7*7/2 and 3*3/2, the common pattern being x*x/2. In this case, the abstraction capabilities of With are not sufficient. One way to enable more powerful abstractions are functions. Depending on the context of use and the interaction with other language features (such as imperative features or objects), functions are also sometimes called procedures or methods. Here we consider so-called first-order functions, that - unlike higher-order functions - are not expressions and can hence not be passed to or be returned from other functions. First-order functions are simply called by name. To introduce first-order functions, we need two new things: The possibility to define functions, and the possibility to call functions. A call to a function is an expression, whereas functions are defined separately. Functions can have an arbitrary number of arguments. The following definitions are the language we have analyzed so far together with the new language constructs for first-order functions:

object Syntax {
  enum Exp:
    case Num(n: Int) extends Exp
    case Add(lhs: Exp, rhs: Exp) extends Exp
    case Mul(lhs: Exp, rhs: Exp) extends Exp
    case Id(x: String) extends Exp
    case With(x: String, xdef: Exp, body: Exp) extends Exp

    /** The new language constructs for first-order functions: */
    case Call(f: String, args: List[Exp]) extends Exp // functions are called by name

  object Exp:
    /** The new language constructs for first-order functions: */
    case class FunDef(args: List[String], body: Exp)
    type Funs = Map[String, FunDef]

    /** We use implicits again to make example programs less verbose. */
    implicit def num2exp(n: Int): Exp = Num(n)
    implicit def string2exp(x: String): Exp = Id(x)
}
import Syntax._
import Exp._

A function has a number of formal args and a body. Note that a first-order function also has a name. To make the invariant that there can only be one function for each name explicit, we have stored functions in the form of a map from function names to FunDefs above.

The substitution for the new language is a straightforward extension of the former one.

def subst(e: Exp, i: String, v: Num): Exp =  e match {
    case Num(n) => e
    case Id(x) => if (x == i) v else e
    case Add(l, r) => Add(subst(l, i, v), subst(r, i, v))
    case Mul(l, r) => Mul(subst(l, i, v), subst(r, i, v))
    case With(x, xdef, body) => With(x,
                                   subst(xdef, i, v),
                                   if (x == i) body else subst(body, i, v))
    case Call(f, args) => Call(f, args.map(subst(_, i, v)))
}

We will first study a "reference interpreter" based on substitution. We pass the map of functions as an additional parameter.

def eval(funs: Funs, e: Exp): Int = e match {
  case Num(n) => n
  case Id(x) => sys.error("unbound identifier: " + x)
  case Add(l, r) => eval(funs, l) + eval(funs, r)
  case Mul(l, r) => eval(funs, l) * eval(funs, r)
  case With(x, xdef, body) => eval(funs, subst(body, x, Num(eval(funs, xdef))))
  case Call(f, args) => {
     val fd = funs(f) // lookup function definition
     val vargs = args.map(eval(funs, _)) // evaluate function arguments
     if (fd.args.size != vargs.size)
       sys.error("number of paramters in call to " + f + " does not match")
     // We construct the function body to be evaluated by subsequently substituting
     // all formal arguments with their respective argument values.
     // If we have only a single argument "fd.arg" and a single argument value "varg",
     // the next line of code is equivalent to:
     // val substbody = subst(fd.body, fd.arg, Num(varg))
     val substbody = fd.args.zip(vargs).foldLeft(fd.body)((b, av) => subst(b, av._1, Num(av._2)))
     eval(funs, substbody)
  }
}

Is the extension really so straightforward? It can be seen in the last line of our definition for subst that variable substitution deliberately ignores the function name f. The substitution for f instead is handled separately inside eval. We say in this case that function names and variable names live in different "name spaces". An alternative would be to have them share one namespace. As an exercise, think about how to support a common namespace for function names and variable names.

A test case:

val someFuns: Funs = Map("adder" -> FunDef(List("a", "b"), Add("a", "b")),
                     "doubleadder" -> FunDef(List("a", "x"), Add(Call("adder", List("a", 5)), Call("adder", List("x", 7)))))
val callSomeFuns = eval(someFuns, Call("doubleadder", List(2, 3)))
// callSomeFuns: Int = 17
assert(callSomeFuns == 17)

The scope of function definitions:

As can be seen in the example above, each function can "see" the other functions. We say that in this language functions have a global scope. Exercise: Can a function also invoke itself? Is this useful? We will now study an environment-based version of the interpreter. To motivate environments, consider the following sample program:

val testProg = With("x", 1, With("y", 2, With("z", 3, Add("x", Add("y", "z")))))

When considering the With case of the interpreter, the interpreter will subsequently produce and evaluate the following intermediate expressions:

val testProgAfterOneStep     = With("y", 2, With("z", 3, Add(1, Add("y", "z"))))
val testProgAfterTwoSteps    = With("z", 3, Add(1, Add(2, "z")))
val testProgAfterThreeSteps  = Add(1, Add(2, 3))

At this point only pure arithmetic is left. But we see that the interpreter had to apply subsitution three times. In general, if the program size is n, then the interpreter may perform up to O(n) substitutions, each of which takes O(n) time. This quadratic complexity seems rather wasteful. Can we do better? We can avoid the redundancy by deferring the substitutions until they are really needed. Concretely, we define a repository of deferred substitutions, called environment. It tells us which identifiers are supposed to be eventually substituted by which value. This idea is captured in the following type definition:

type Env = Map[String, Int]

Initially, we have no substitutions to perform, so the repository is empty. Every time we encounter a construct (a With or an application Call) that requires substitution, we augment the repository with one more entry, recording the identifier’s name and the value (if eager) or expression (if lazy) it should eventually be substituted with. We continue to evaluate without actually performing the substitution. This strategy breaks a key invariant we had established earlier, which is that any identifier the interpreter could encounter must be free, for had it been bound, it would have already been substituted. Now that we’re no longer using the substitution-based model, we may encounter bound identifiers during interpretation. How do we handle them? We must substitute them by consulting the repository.

def evalWithEnv(funs: Funs, env: Env, e: Exp): Int = e match {
  case Num(n) => n
  case Id(x) => env(x) // look up in repository of deferred substitutions
  case Add(l, r) => evalWithEnv(funs, env, l) + evalWithEnv(funs, env, r)
  case Mul(l, r) => evalWithEnv(funs, env, l) * evalWithEnv(funs, env, r)
  case With(x, xdef, body) => evalWithEnv(funs, env + ((x, evalWithEnv(funs, env, xdef))), body)
  case Call(f, args) => {
     val fd = funs(f) // lookup function definition
     val vargs = args.map(evalWithEnv(funs, env, _)) // evaluate function arguments
     if (fd.args.size != vargs.size)
       sys.error("number of paramters in call to " + f + " does not match")
     // We construct the environment by associating each formal argument to its actual value
     val newenv = Map() ++ fd.args.zip(vargs)
     evalWithEnv(funs, newenv, fd.body)
  }
}

val evalEnvSomeFuns = evalWithEnv(someFuns, Map.empty, Call("doubleadder", List(2, 3)))
// evalEnvSomeFuns: Int = 17
assert(evalEnvSomeFuns == 17)

In the interpreter above, we have extended the empty environment when constructing newenv. A conceivable alternative is to extend env instead, like so:

def evalDynScope(funs: Funs, env: Env, e: Exp): Int = e match {
  case Num(n) => n
  case Id(x) => env(x)
  case Add(l, r) => evalDynScope(funs, env, l) + evalDynScope(funs, env, r)
  case Mul(l, r) => evalDynScope(funs, env, l) * evalDynScope(funs, env, r)
  case With(x, xdef, body) => evalDynScope(funs, env + ((x, evalDynScope(funs, env, xdef))), body)
  case Call(f, args) => {
     val fd = funs(f)
     val vargs = args.map(evalDynScope(funs, env, _))
     if (fd.args.size != vargs.size)
       sys.error("number of paramters in call to " + f + " does not match")
     val newenv = env ++ fd.args.zip(vargs) // extending env instead of Map() !!
     evalDynScope(funs, newenv, fd.body)
  }
}

val evalDynSomeFuns = evalDynScope(someFuns, Map.empty, Call("doubleadder", List(2, 3)))
// evalDynSomeFuns: Int = 17
assert(evalDynSomeFuns == 17)

Does this make a difference? Yes, it does. Here is an example:

val funnyFun: Funs = Map("funny" -> FunDef(List("a"), Add("a", "b")))
val evalDynFunnyFun = evalDynScope(funnyFun, Map.empty, With("b", 3, Call("funny", List(4))))
// evalDynFunnyFun: Int = 7
assert(evalDynFunnyFun == 7)

Obviously this interpreter is "buggy" in the sense that it does not agree with the substitution-based interpreter. But is this semantics reasonable? Let's introduce some terminology to make the discussion simpler:

Definition (Static Scope): In a language with static scope, the scope of an identifier’s binding is a syntactically delimited region. A typical region would be the body of a function or other binding construct.

Definition (Dynamic Scope): In a language with dynamic scope, the scope of an identifier’s binding is the entire remainder of the execution during which that binding is in effect.

We see that eval and evalWithEnv give our language static scoping, whereas evalDynScope gives our language dynamic scoping. Armed with this terminology, we claim that dynamic scope is entirely unreasonable. The problem is that we simply cannot determine what the value of a program will be without knowing everything about its execution history. If a function f were invoked by some sequence of other functions that did not bind a value for some parameter of f, then that particular application of f would result in an error, even though a previous application of f in the very same program’s execution may have completed successfully! In other words, simply by looking at the source text of f, it would be impossible to determine one of the most rudimentary properties of a program: whether or not a given identifier was bound. You can only imagine the mayhem this would cause in a large software system, especially with multiple developers and complex flows of control. We will therefore regard dynamic scope as an error. That said, there are facets of dynamic binding that are quite useful. For instance, exception handlers are typically dynamically scoped: A thrown exception is dispatched to the most recently encountered active exception handler for that exception type.

Higher-Order Functions

The content of this chapter is available as a Scala file here.

Higher-Order Functions

F1WAE, the language with first-order functions, lets us abstract over patterns that involve numbers. But what if we want to abstract over patterns that involve functions, such as the "list-fold" pattern, whose instantiations include summing or multiplying a list of integers?

To enable this kind of abstraction, we need to make functions "first-class", which means that they become values that can be passed to or returned from functions or stored in data structures. Languages with first-class functions enable so-called "higher-order functions", which are functions that accept or return a (possibly again higher-order) function.

We will see that this extension will make our language both simpler and much more powerful. This seeming contradiction is famously addressed by the first sentence of the Scheme language specification:

"Programming languages should be designed not by piling feature on top of feature, but by removing the weaknesses and restrictions that make additional features appear necessary."

The simplicity is due to the fact that this language is so expressive that many other language features can be "encoded", i.e., they do not need to be added to the language but can be expressed with the existing features.

This language, which we call "FAE", is basically the so-called "lambda calculus", a minimal but powerful programming language that has been highly influential in the design and theory of programming languages.

FAE is the language of arithmetic expressions, AE, plus only two additional language constructs: Function abstraction and function application.

object Syntax {
  enum Exp:
    case Num(n: Int)
    case Id(name: String)
    case Add(lhs: Exp, rhs: Exp)

    // Both function definitions and applications are expressions.
    case Fun(param: String, body: Exp)
    case Ap(funExpr: Exp, argExpr: Exp)

  object Exp:
    implicit def num2exp(n: Int): Exp = Num(n)
    implicit def id2exp(s: String): Exp = Id(s)
    // "with" would be a better name for this function, but it is reserved in Scala
    def wth(x: String, xdef: Exp, body: Exp): Exp = Ap(Fun(x, body), xdef)
}

import Syntax._
import Exp._

Due to the lambda calculus, the concrete syntax for function abstraction is often written with a lambda, such as lambda x. x + 3, thus also called lambda abstraction. The Scala syntax for lambda terms is (x) => x + 3, the Haskell syntax is \x -> x + 3.

The concrete syntax for function application is often either juxtaposition f a or using brackets f(a). Haskell and the lambda calculus use the former, Scala uses the latter.

The with construct is not needed anymore since it can be encoded using Ap and Fun. For instance, with x = 7 in x + 3 can be encoded (using Scala syntax) as ((x) => x + 3)(7). We have made this idea explicit above by giving a constructive translation. Remember that such translations are often called "desugaring".

As for F1WAE, we will at first define the meaning of FAE in terms of substitution. Here is the substitution function for FAE.

def subst0(e1: Exp, x: String, e2: Exp): Exp = e1 match {
  case Num(n) => e1
  case Add(l, r) => Add(subst0(l, x, e2), subst0(r, x, e2))
  case Id(y) => if (x == y) e2 else Id(y)
  case Ap(f, a) => Ap(subst0(f, x, e2), subst0(a, x, e2))
  case Fun(param, body) =>
    if (param == x) e1  else Fun(param, subst0(body, x, e2))
}

Let's try whether subst0 produces reasonable results.

assert(subst0(Add(5, "x"), "x", 7) == Add(5, 7))
assert(subst0(Add(5, "x"), "y", 7) == Add(5, "x"))
assert(subst0(Fun("x", Add("x", "y")), "x", 7) == Fun("x", Add("x", "y")))

Looks good. However, what happens if e2 contains free variables? The danger here is that they may be accidentially "captured" by the substitution. For instance, consider

val subst0Test = subst0(Fun("x", Add("x", "y")), "y", Add("x", 5))
// subst0Test: Exp = Fun(
//   param = "x",
//   body = Add(
//     lhs = Id(name = "x"),
//     rhs = Add(lhs = Id(name = "x"), rhs = Num(n = 5))
//   )
// )

The result is Fun("x", Add("x", Add("x", 5))) This is not desirable, since it again violates static scoping.

Note that this problem did not show up in earlier languages, because there we only substituted variables by numbers, but not by expressions that may contain free variables: The type of e2 was Num and not Exp.

Hence we are still not done with defining substitution. But what is the desired result of the substitution above? The answer is that we must avoid the name clash by renaming the variable bound by the "lambda" if the variable name occurs free in e2. This new variable name should be "fresh", i.e., not occur free in e2.

For instance, in the example above, we could first rename "x" to the fresh name "x0" and only then substitute, i.e.

subst(Fun("x", Add("x", "y")), "y", Add("x", 5)) == Fun("x0", Add("x0", Add("x", Num(5))))

Let's do this step by step.

def freshName(names: Set[String], default: String): String = {
  var last: Int = 0
  var freshName = default
  while (names contains freshName) { freshName = default + last; last += 1; }
  freshName
}

val freshNameExa1 = freshName(Set("y", "z"), "x")
// freshNameExa1: String = "x"
val freshNameExa2 = freshName(Set("x2", "x0", "x4", "x", "x1"), "x")
// freshNameExa2: String = "x3"
assert(freshNameExa1 == "x")
assert(freshNameExa2 == "x3")

def freeVars(e: Exp): Set[String] =  e match {
   case Id(x) => Set(x)
   case Add(l, r) => freeVars(l) ++ freeVars(r)
   case Fun(x, body) => freeVars(body) - x
   case Ap(f, a) => freeVars(f) ++ freeVars(a)
   case Num(n) => Set.empty
}

val freeVarsExa = freeVars(Fun("x", Add("x", "y")))
// freeVarsExa: Set[String] = Set("y")
assert(freeVarsExa == Set("y"))

def subst(e1: Exp, x: String, e2: Exp): Exp = e1 match {
  case Num(n) => e1
  case Add(l, r) => Add(subst(l, x, e2), subst(r, x, e2))
  case Id(y) => if (x == y) e2 else Id(y)
  case Ap(f, a) => Ap(subst(f, x, e2), subst(a, x, e2))
  case Fun(param, body) =>
    if (param == x) e1 else {
      val fvs = freeVars(Fun(param, body)) ++ freeVars(e2) + x
      val newvar = freshName(fvs, param)
      Fun(newvar, subst(subst(body, param, Id(newvar)), x, e2))
    }
}

assert(subst(Add(5, "x"), "x", 7) == Add(5, 7))
assert(subst(Add(5, "x"), "y", 7) == Add(5, "x"))
assert(subst(Fun("x", Add("x", "y")), "x", 7) == Fun("x", Add("x", "y")))
// test capture-avoiding substitution
assert(subst(Fun("x", Add("x", "y")), "y", Add("x", 5)) == Fun("x0", Add(Id("x0"), Add(Id("x"), Num(5)))))
assert(subst(Fun("x", Add(Id("x0"), Id("y"))), "y", Add(Id("x"), 5)) == Fun("x1", Add(Id("x0"), Add(Id("x"), Num(5)))))

OK, equipped with this new version of substitution we can now define the interpreter for this language. But how do we evaluate a function abstraction? Obviously we cannot return a number.

We realize that functions are also values! Hence we have to broaden the return type of our evaluator to also allow functions as values. For simplicity, we use Exp as our return type since it allows us to return both numbers and functions. Later we will become more sophisticated.

This means that a new class of errors can occur: A subexpression evaluates to a number where a function is expected, or vice versa. Such errors are called type errors, and we will talk about them in much more detail later.

For now, it means that we need to analyze (typically by pattern matching) the result of recursive invocations of eval to check whether the result has the right type.

The remainder of the interpreter is unsurprising:

def eval(e: Exp): Exp = e match {
  case Id(v) => sys.error("unbound identifier: " + v)
  case Add(l, r) => (eval(l), eval(r)) match {
                     case (Num(x), Num(y)) => Num(x + y)
                     case _ => sys.error("can only add numbers")
                    }
  case Ap(f, a) => eval(f) match {
     case Fun(x, body) => eval(subst(body, x, eval(a)))  // call-by-value
     // case Fun(x, body) => eval(subst(body, x, a))        // call-by-name
     case _ => sys.error("can only apply functions")
  }
  case _ => e // numbers and functions evaluate to themselves
}

We can also make the return type more precise to verify the invariant that numbers and functions are the only values.

def eval2(e: Exp): Either[Num, Fun] = e match {
  case Id(v) => sys.error("unbound identifier: " + v)
  case Add(l, r) => (eval2(l), eval2(r)) match {
                     case (Left(Num(x)), Left(Num(y))) => Left(Num(x + y))
                     case _ => sys.error("can only add numbers")
                    }
  case Ap(f, a) => eval2(f) match {
     case Right(Fun(x, body)) => eval2(subst(body, x, eval(a)))
     case _ => sys.error("can only apply functions")
  }
  case f@Fun(_, _) => Right(f)
  case n@Num(_) => Left(n)
}

Let's test:

val test = Ap(Fun("x", Add("x", 5)), 7)
assert(eval(test) == Num(12))

FAE is a computationally (Turing-)complete language. For instance, we can define a non-terminating program. This program is commonly called Omega:

val omega = Ap(Fun("x", Ap("x", "x")), Fun("x", Ap("x", "x")))
// try eval(omega) to crash the interpreter ;-)

Omega can be extended to yield a fixed-point combinator, which can be used to encode arbitrary recursive functions. We'll come back to this topic later.

Let's now discuss what an environment-based version of this interpreter looks like. Here is a first attempt:

type Env0 = Map[String, Exp]

def evalWithEnv0(e: Exp, env: Env0): Exp = e match {
  case Id(x) => env(x)
  case Add(l, r) => {
    (evalWithEnv0(l, env), evalWithEnv0(r, env)) match {
      case (Num(v1), Num(v2)) => Num(v1 + v2)
      case _ => sys.error("can only add numbers")
    }
  }
  case Ap(f, a) => evalWithEnv0(f, env) match {
    case Fun(x, body) => evalWithEnv0(body, Map(x -> evalWithEnv0(a, env)))
    case _ => sys.error("can only apply functions")
  }
  case _ => e // numbers and functions evaluate to themselves
}

assert(evalWithEnv0(test, Map.empty) == Num(12))

However, consider the following example:

val test2 = wth("x", 5, Ap(Fun("f", Ap("f", 3)), Fun("y", Add("x", "y"))))

It works fine in the substitution-based interpreter,

val evalTest2 = eval(test2)
// evalTest2: Exp = Num(n = 8)
assert(evalTest2 == Num(8))

but

val evalEnv0Test2 = evalWithEnv0(test2, Map.empty)
// java.util.NoSuchElementException: key not found: x
// 	at scala.collection.immutable.Map$Map1.apply(Map.scala:243)
// 	at repl.MdocSession$MdocApp.evalWithEnv0(higher-order-functions.md:208)
// 	at repl.MdocSession$MdocApp.evalWithEnv0(higher-order-functions.md:210)
// 	at repl.MdocSession$MdocApp.evalWithEnv0(higher-order-functions.md:216)
// 	at repl.MdocSession$MdocApp.evalWithEnv0(higher-order-functions.md:216)
// 	at repl.MdocSession$MdocApp.evalWithEnv0(higher-order-functions.md:216)
// 	at repl.MdocSession$MdocApp.$init$$$anonfun$1(higher-order-functions.md:248)
// 	at repl.MdocSession$MdocApp.$init$$$anonfun$adapted$1(higher-order-functions.md:249)

yields a key not found: x error. The problem is that we have forgotten the deferred substitutions to be performed in the body of the function.

What can we do to fix this problem? We could try to replace the second line in the Ap case by

case Fun(x, body) => evalWithEnv0(body, env + (x -> evalWithEnv0(a, env)))

but this would again introduce dynamic scoping.

Hence, when we evaluate a function, we do not only have to store the function, but also the environment active when the function was created. This pair of function and environment is called a closure. The environment stored in the closure is used when the function is eventually applied.

Hint: If you cannot answer what a closure is and how it is used in the interpreter, you will be toast in the exam! Since closures are not expressible in the language syntax, we now come to the point where we need a separate category of values. The values in FAE can be either numbers or closures.

sealed abstract class Value
type Env = Map[String, Value]
case class NumV(n: Int) extends Value
case class ClosureV(f: Fun, env: Env) extends Value

The evaluator becomes:

def evalWithEnv(e: Exp, env: Env): Value = e match {
  case Num(n: Int) => NumV(n)
  case Id(x) => env(x)
  case Add(l, r) => {
    (evalWithEnv(l, env), evalWithEnv(r, env)) match {
      case (NumV(v1), NumV(v2)) => NumV(v1 + v2)
      case _ => sys.error("can only add numbers")
    }
  }
  case f@Fun(param, body) => ClosureV(f, env)
  case Ap(f, a) => evalWithEnv(f, env) match {
    // Use environment stored in closure to realize proper lexical scoping!
    case ClosureV(f, closureEnv) => evalWithEnv(f.body, closureEnv + (f.param -> evalWithEnv(a, env)))
    case _ => sys.error("can only apply functions")
  }
}
assert(evalWithEnv(test, Map.empty) == NumV(12))
assert(evalWithEnv(test2, Map.empty) == NumV(8))

Lazy Evaluation

The content of this chapter is available as a Scala file here.

Motivation for Lazy Evaluation

Read "Why Functional Programming Matters" by John Hughes available at https://www.cse.chalmers.se/~rjmh/Papers/whyfp.html.

What lazy evaluation means:

The choice of evaluation strategy is a purely semantic change that requires no change of the syntax. For this reason we reuse the syntactic definitions of FAE, hence load higher-order-functions.scala before executing this script. Before we discuss lazy evaluation, we will first discuss a related evaluation strategy, call-by-name.

Call-by-name can be explained very succintly in the substitution-based interpreter: Instead of evaluating the argument a in the Ap case before substitution, we substitute the unevaluated argument into the body. The rest remains exactly the same.

def evalcbn(e: Exp): Exp = e match {
  case Id(v) => sys.error("unbound identifier: " + v)
  case Add(l, r) => (evalcbn(l), evalcbn(r)) match {
                     case (Num(x), Num(y)) => Num(x + y)
                     case _ => sys.error("can only add numbers")
                    }
  case Ap(f, a) => evalcbn(f) match {
     case Fun(x, body) => evalcbn(subst(body, x, a)) // no evaluation of a!
     case _ => sys.error("can only apply functions")
  }
  case _ => e
}

Does this change the semantics, or is it just an implementation detail? In other words, is eval(e) == evalcbn(e) for all e ? Let's try two former test cases.

assert(evalcbn(test) == eval(test))
assert(evalcbn(test2) == eval(test2))

One can formally prove that if eval and evalcbn both produce a number then the numbers are equal. Do they also agree if they produce a function? Not necessarily. Consider:

val test3 = Ap(Fun("f", Fun("x", Ap("f", "x"))), Add(1, 2))

assert(eval(test3) == Fun("x", Ap(Num(3), Id("x"))))
assert(evalcbn(test3) == Fun("x", Ap(Add(Num(1), Num(2)), Id("x"))))

However, if both produce a function, then the functions "behave" the same. More specifically, the function bodies produced by eval may be "more evaluated" than those produced by evalcbn since for the latter the evaluation of the arguments substituted for the parameters in the body is deferred. If we evaluated within function bodies (also called evaluation "under a lambda") - which our interpreters do not do - we could produce the expression returned from eval from the expression returned by evalcbn. This kind of equivalence is also called "beta-equivalence".

Most importantly, however, eval and evalcbn differ with regard to their termination behavior. We have seen that omega is a diverging expression. In eval, the term

 val test4 = Ap(Fun("x", 5), omega)

is hence also diverging. In contrast:

assert(evalcbn(test4) == Num(5))

Extra material: Infinite lists in FAE (not relevant for exam)

Using our call-by-name interpreter, we can express the same kinds of programming patterns that we have tried in Haskell, such as infinite lists.

We do not have direct support for lists, but we can encode lists. This kind of encoding is called Church encoding.

val nil = Fun("c", Fun("e", "e"))
val cons  = Fun("x", Fun("xs", Fun("c", Fun("e", Ap(Ap("c", "x"), Ap(Ap("xs", "c"), "e"))))))

For instance, the list 1, 2, 3 is encoded as:

val list123 = Ap(Ap("cons", 1), Ap(Ap("cons", 2), Ap(Ap("cons", 3), "nil")))

The map function on lists becomes:

val maplist = Fun("f", Fun("l", Ap(Ap("l", Fun("x", Fun("xs", Ap(Ap("cons", Ap("f", "x")), "xs")))), "nil")))

For instance, we can map the successor function over the 1, 2, 3 list.

val test5 = wth("cons", cons,
            wth("nil", nil,
            wth("maplist", maplist,
            Ap(Ap("maplist", Fun("x", Add("x", 1))), list123))))

Since it is somewhat difficult to print out the resulting list in our primitive language we construct the result we expect explicitly.

val test5res = wth("cons", cons,
               wth("nil", nil,
                 Ap(Ap("cons", 2), Ap(Ap("cons", 3), Ap(Ap("cons", 4), "nil")))))
assert(eval(test5) == eval(test5res))

Using evalcbn instead of eval the assertion does not hold (why?), but the results are beta-equivalent. We can also construct infinite lists. To this end, we need some form of recursion. We choose the standard fixed-point operator Y. This operator only works under call-by-name or other so-called "non-strict" evaluation strategies.

val y = Fun("f", Ap(Fun("x", Ap("f", Ap("x", "x"))), Fun("x", Ap("f", Ap("x", "x")))))

Using Y, we can construct infinite lists, such as the list of all natural numbers.

val allnats = Ap(Ap("y", Fun("nats", Fun("n", Ap(Ap("cons", "n"), Ap("nats", Add("n", 1)))))), 1)

We can also perform standard computations on infinite lists, such as mapping the successor function over it.

val list2toinfty = wth("cons", cons,
                   wth("nil", nil,
                   wth("y", y,
                   wth("maplist", maplist,
                      Ap(Ap("maplist", Fun("x", Add("x", 1))), allnats)))))

Of course, list2toinfty diverges when we use eval, but it works fine with evalcbn. It is hard to verify the result due to an almost unreadable output. Hence we propose the following

Exercise: Extend the language such that you can implement the "take" function as known from Haskell within the language (if0-expressions or something like it are needed for that). Now add a "print" function that prints a number on the console. Use it to display that the first 3 list elements of list2toinfty are 2, 3, 4.

end of extra material

Environment-based lazy evaluation

Let us now consider the question how we can implement call-by-name in the environment-based interpreter. Translating the idea of not evaluating the function argument to the environment-based version seems to suggest that the environment should map identifiers to expressions instead of values.

However, we run into the same problems that we had with first-class functions before we introduced closures: What happens to the deferred substitutions that still have to be applied in the function argument? If we discard the environment in which the function argument was defined we again introduce a variant of dynamic scoping.

Hence, like for closures, we need to store the environment together with the expression. We call such a pair a thunk. An environment thus becomes a mapping from symbols to thunks. Note that environments and thunks are hence mutually recursive. In Scala, we can hence not use type definitions of the form

   type Thunk = (Exp, Env)
   type Env = Map[String, Thunk]

Instead, we use a Scala class Env to express this recursion. Since we want to experiment with different variants of how to generate and evaluate thunks we first create a parameterizable variant of the evaluator that leaves open how to

  1. represent thunks (type Thunk)
  2. create thunks (method delay)
  3. evaluate thunks (method force).

Hint: Research on the internet what abstract type members in Scala are. For instance, here.

trait CBN {
    type Thunk

    def delay(e: Exp, env: EnvThunk): Thunk
    def force(t: Thunk): ValueCBN

    case class EnvThunk(map: Map[String, Thunk]) {
      def apply(key: String): Thunk = map.apply(key)
      def +(other: (String, Thunk)): EnvThunk = EnvThunk(map + other)
    }

    // since values also depend on EnvThunk and hence on Thunk they need to
    // be defined within this trait
    sealed abstract class ValueCBN
    case class NumV(n: Int) extends ValueCBN
    case class ClosureV(f: Fun, env: EnvThunk) extends ValueCBN
    def evalCBN(e: Exp, env: EnvThunk): ValueCBN = e match {
      case Id(x) => force(env(x)) // force evaluation of thunk if identifier is evaluated
      case Add(l, r) => {
        (evalCBN(l, env), evalCBN(r, env)) match {
          case (NumV(v1), NumV(v2)) => NumV(v1 + v2)
          case _ => sys.error("can only add numbers")
        }
      }
      case Ap(f, a) => evalCBN(f, env) match {
        // delay argument expression and add it to environment of the closure
        case ClosureV(f, cenv) => evalCBN(f.body, cenv + (f.param -> delay(a, env)))
        case _ => sys.error("can only apply functions")
      }
      case Num(n) => NumV(n)
      case f@Fun(x, body) => ClosureV(f, env)
    }
}

Let's now create an instance of CBN that corresponds to the substitution-based call-by-name interpreter. A thunk is just a pair of expression and environment. Forcing a thunk just evaluates it in the stored environment. To understand what is going on during evaluation of tests we trace argument evaluation by a printout to the console.

object CallByName extends CBN {
  type Thunk = (Exp, EnvThunk)
  def delay(e: Exp, env: EnvThunk) = (e, env)
  def force(t: Thunk) = {
    println("Forcing evaluation of expression: " + t._1)
    evalCBN(t._1, t._2)
  }
}

assert(CallByName.evalCBN(test, CallByName.EnvThunk(Map.empty)) == CallByName.NumV(12))
// Forcing evaluation of expression: Num(7)

Call-by-need

Call-by-name is rather wasteful: If an argument is used n times in the body, the argument expression is re-evaluated n times. For instance, in

val cbntest = wth("double", Fun("x", Add("x", "x")),
               Ap("double", Add(2, 3)))

the sum of 2 and 3 is computed twice. If the argument is passed again to another function, this may lead to an exponential blow-up.

Example:

val blowup  = wth("a", Fun("x", Add("x", "x")),
              wth("b", Fun("x", Add(Ap("a", "x"), Ap("a", "x"))),
              wth("c", Fun("x", Add(Ap("b", "x"), Ap("b", "x"))),
              wth("d", Fun("x", Add(Ap("c", "x"), Ap("c", "x"))),
              wth("e", Fun("x", Add(Ap("d", "x"), Ap("d", "x"))),
              wth("f", Fun("x", Add(Ap("e", "x"), Ap("e", "x"))),
              Ap("f", Add(2, 3))))))))

Can we do better? Yes, by caching the value when the argument expression is evaluated for the first time. This evaluation strategy is called call-by-need.

Caching is easy to implement in Scala:

object CallByNeed extends CBN {
  case class MemoThunk(e: Exp, env: EnvThunk) {
    var cache: ValueCBN = null
  }
  type Thunk = MemoThunk
  def delay(e: Exp, env: EnvThunk) = MemoThunk(e, env)
  def force(t: Thunk) = {
    if (t.cache == null) {
      println("Forcing evaluation of expression: " + t.e)
      t.cache = evalCBN(t.e, t.env)
    } else {
      println ("Reusing cached value " + t.cache + " for expression " + t.e)
    }
    t.cache
  }
}

For instance, compare call-by-need and call-by-name in cbntest or blowup. However, the meta-language (i.e., the subset of Scala features) used in the interpreter has become more complicated:

Since we are using mutation, the order of evaluation and aliasing of object references becomes important. Luckily, call-by-need agrees with call-by-name with regard to produced values and termination behavior, hence it is usually not necessary to reason about programs with the call-by-need semantics. If, however, one wants to reason about the performance of a program in a call-by-need setting, one has to take these additional complications into account. In practice, it is even worse, since languages like Haskell perform additional optimizations that, for instance, switch to call-by-value if an analysis can determine that an argument will definitely be used (look up "strictness analysis").

Topics for class discussion:

  • Is it a good idea to mix a language with implicit mutation (such as Java, Scala, C++, Python, ...) with lazy evaluation?
  • How can one simulate lazy evaluation in an eager language? Basic idea: 'Lambda' as evaluation firewall.

Recursive Functions

The content of this chapter is available as a Scala file here.

Recursion

Let's try to write a function that computes the sum of the first \( n \) integers. Let's pretend we do not know that the sum of the first \( n \) integers is \( n * (n + 1) / 2 \) and instead compute the sum in a loop. Let's try to do this in FAE (with If0):

val sumattempt = wth("sum", Fun("n", If0("n", 0, Add("n", Ap("sum", Add("n", -1))))), Ap("sum", 10))

sumattempt won't work and yield an unbound identifier error (why?). An alternative would be to use a variant of the y combinator to support recursion properly, but today we want to talk about direct support for recursion. More specifically, we want a language construct Letrec that is similar to with, except that the bound String can be used in the expression the String is bound to:

Letrec(x: String, e: Exp, body: Exp)

With this new construct our language looks like this:

object Syntax {
  enum Exp:
    case Num(n: Int)
    case Id(name: String)
    case Add(lhs: Exp, rhs: Exp)
    case If0(cond: Exp, thenExp: Exp, elseExp: Exp)
    case Fun(param: String, body: Exp)
    case Ap(funExpr: Exp, argExpr: Exp)
    case Letrec(x: String, e: Exp, body: Exp)

  object Exp:
    implicit def num2exp(n: Int): Exp = Num(n)
    implicit def id2exp(s: String): Exp = Id(s)
    def wth(x: String, xdef: Exp, body: Exp): Exp = Ap(Fun(x, body), xdef)
}

import Syntax._
import Exp._

Using Letrec, our example can be expressed as follows.

val sum = Letrec("sum", Fun("n", If0("n", 0, Add("n", Ap("sum", Add("n", -1))))), Ap("sum", 10))

Let's now consider the semantics of Letrec. Consider the evaluation of Letrec(x, e, body) in an environment env. What environment should we use to evaluate e and body, respectively? Using env for e will produce a ClosureV(Fun("n", ..."sum"'...), env), and hence the environment when evaluating body will be envbody = env + (x -> ClosureV(Fun("n", ..."sum"...), env)).

This is bad, because the env in the closure does not contain a binding for "sum" and hence the recursive invocation will fail. The environment in the closure must contain a mapping for "sum". Hence envbody should look like

  envbody = env + (x -> ClosureV(Fun("n", ..."sum"...),
                                 env + ("sum" -> ClosureV(Fun("n", ..."sum"...), env)))

This looks better, but now the second closure contains an environment with no binding of "sum". What we need is an environment that satisfies the equation:

  envbody == env + (x -> ClosureV(Fun("n", ..."sum"..), envbody))

Obviously envbody must be circular. There are different ways to create such a circular environment. We will choose mutation to create a circle. More specifically, we introduce a mutable pointer to a value class ValuePointer which will be initialized with a null pointer.

In the Letrec case, we put such a ValuePointer in the environment and evaluate the (recursive) expression in that environment. Then we update the pointer with the result of evaluating that expression.

The only other change we need to make is to dereference a potential value pointer in the Id case. We deal with the necessary case distinction by a polymorphic value method. Due to the mutual recursion between ValueHolder and Value the definitions are put into an object.

object Values {
  trait ValueHolder {
    def value: Value
  }
  sealed abstract class Value extends ValueHolder { def value = this }
  case class ValuePointer(var v: Value) extends ValueHolder { def value = v }
  case class NumV(n: Int) extends Value
  case class ClosureV(f: Fun, env: Env) extends Value
  type Env = Map[String, ValueHolder]
}

import Values._  // such that we do not have to write Values.ValueHolder etc.

The interpreter is unchanged except for the additional Letrec case and the modified Id case.

def eval(e: Exp, env: Env): Value = e match {
  case Num(n: Int) => NumV(n)
  case Id(x) => env(x).value  // dereference potential ValuePointer
  case If0(cond, thenExp, elseExp) => eval(cond, env) match {
    case NumV(0) => eval(thenExp, env)
    case _ => eval(elseExp, env)
  }
  case Add(l, r) => {
    (eval(l, env), eval(r, env)) match {
      case (NumV(v1), NumV(v2)) => NumV(v1 + v2)
      case _ => sys.error("can only add numbers")
    }
  }
  case f@Fun(param, body) => ClosureV(f, env)
  case Ap(f, a) => eval(f, env) match {
    case ClosureV(f, closureEnv) => eval(f.body, closureEnv + (f.param -> eval(a, env)))
    case _ => sys.error("can only apply functions")
  }
  case Letrec(x, e, body) => {
    val vp = ValuePointer(null)  // initialize pointer with null
    val newenv = env + (x -> vp)  // evaluate e in the environment extended with the placeholder
    vp.v = eval(e, newenv)         // create the circle in the environment
    eval(body, newenv) // evaluate body in circular environment
  }
}

The sum of numbers from 1 to 10 should be 55.

assert(eval(sum, Map.empty) == NumV(55))

// These test cases were contributed by rzhxeo (Sebastian Py)
var func = Fun("n", If0("n", 0, Ap("func", Add("n", -1))))
var test1 = Letrec("func", func, Ap("func", 1))
var test2 = Letrec("func", Ap(Fun("notUsed", func), 0), Ap("func", 1))
assert(eval(test1, Map()) == NumV(0))
assert(eval(test2, Map()) == NumV(0))

Mutation

The content of this chapter is available as a Scala file here.

Today we study mutation. More specifically, we want to equip our language with mutable data structures.

Typical mutable data structures in common languages include objects with mutable fields or structures/records in languages like C or Pascal. We will study a particularly simple mutable data structure: Boxes. In OO parlance, boxes can be thought of as an object with a single field that can be mutated. Despite their simplicity, boxes already illustrate all main issues associated with adding mutable state to a language.

A different and less interesting form of mutation is the mutability of variables, such as the possibility to assign something to a 'local' variable bound via a lambda or with. We will not talk about mutable variables today. We will add boxes to our base language, FAE.

object Syntax {
  enum Exp:
    case Num(n: Int)
    case Id(name: String)
    case Add(lhs: Exp, rhs: Exp)
    case Mul(lhs: Exp, rhs: Exp)
    case If0(cond: Exp, thenExp: Exp, elseExp: Exp)
    case Fun(param: String, body: Exp)
    case Ap(funExpr: Exp, argExpr: Exp)

    /** To add mutation to FAE, we add four language constructs: */
    case NewBox(e: Exp) // create a new box
    case SetBox(b: Exp, e: Exp) // assign to a box
    case OpenBox(b: Exp) // read value in a box
    case Seq(e1: Exp, e2: Exp) // sequencing of expressions

  object Exp:
    implicit def num2exp(n: Int): Exp = Num(n)
    implicit def id2exp(s: String): Exp = Id(s)
    def wth(x: String, xdef: Exp, body: Exp): Exp = Ap(Fun(x, body), xdef)
}
import Syntax._
import Exp._

In this new language, the following sample program,

val test1 = wth("b", NewBox(0),
              Seq(
                SetBox("b", Add(1, OpenBox("b"))),
                OpenBox("b")))

should give as result 1 in a proper implementation. Let's consider how our interpreter could handle sequencing.

Here is an attempt:

  case Seq(e1, e2) => {
    eval(e1, env)
    eval(e2, env)
  }

This cannot be correct. As long as our interpreter does not use mutation, evaluation could not make any changes to the environment, hence there is no way the evaluation of e1 could have any effect on the evaluation of e2.

In order to demostrate the actual nature of mutation, we will not use mutation in our meta-language to implement mutation in our object language. Thus, we will not use a mutable data structure to implement the environment in our interpreter. Instead, one may turn to the so-called environment-passing style, in which the interpreter returns also a possibly updated environment together with the computed value when it evaluates an expression. However, this solution does not always work. Consider the following example:

val test2 = wth("a", NewBox(1),
              wth("f", Fun("x", Add("x", OpenBox("a"))),
                Seq(SetBox("a", 2),
                  Ap("f", 5))))

The mutation should affect the box stored in the closure bound to f. But with the implementation strategy described above it would not. Note that changing the value of a in the example is not a vialation of static scope. Scoping only says where an identifier is bound; it does not say to what an identifier is bound, in particular, whether whatever bound to the identifier is fixed.

Indeed, the variable a is bound to the same box in both the static environment where the function f is created and the dynamic environment where the function f is applied.

As before, when applying the function f to the argument 5, we can choose either

  1. To use the static environment (where the variable a is bound to a boxed 1) stored in the closure created for f.

  2. Or to use the dynamic environment (where the variable a is bound to a boxed 2) present at the time of applying f.

The first choice leads the program to evaluate to 6 rather than the expected 7. The second will record the change to the box, but it reintroduces dynamic scoping. So both choices do not work. Insight: We need two repositories of information. One, the environment, guards static scope.

sealed abstract class Value
type Env = Map[String, Value]
case class NumV(n: Int) extends Value
case class ClosureV(f: Fun, env: Env) extends Value

The other, which we call store, is tracking dynamic changes. Determining the value inside a box will become a two-step process: We first evaluate the box expression to an address, and then use the store to lookup the value stored at that address. We choose to represent addresses by integers.

type Address = Int
case class AddressV(a: Address) extends Value

type Store = Map[Address, Value]

We will often need a fresh address in the store. We do so using a counter variable.

var _nextAddress = 0

def nextAddress: Address = {
  _nextAddress += 1
  _nextAddress
}

Note: We promised to implement the interpreter without using mutation. Here we did use mutation, but this usage of mutation is not essential: we could instead just search for the largest address in the present store and add one to it. Let's now discuss the evaluation of FAE with conditionals and boxes, BCFAE. To this end, consider the following sample program:

val test3 = wth("switch", NewBox(0),
             wth("toggle", Fun("dummy", If0(OpenBox("switch"),
                                          Seq(SetBox("switch", 1), 1),
                                          Seq(SetBox("switch", 0), 0))),
                 Add(Ap("toggle", 42), Ap("toggle", 42))))

This program should return 1. Let's discuss on the blackboard what the environment and store should look like during the evaluation of this program.

IDExpValueEnvStore
Awth(.."switch" -> .1 -> NumV(0)
Bwth(.."toggle" -> ..
CAdd(..
DAp("toggle")11 -> NumV(1)
EAp("toggle")01 -> NumV(0)
FAdd(1, 0)1

Insight: We must pass the current store in to evaluate every expression and pass the possibly updated store out after the evaluation. This is called store-passing style. Consequently, we have to update the type of our evaluator.

def eval(e: Exp, env: Env, s: Store): (Value, Store) = e match {
  /* All expressions whose evaluation does not alter the store just return s. */
  case Num(n) => (NumV(n), s)
  case Id(x) => (env(x), s)
  case f@Fun(_, _) => (ClosureV(f, env), s)


// In recursive cases we have to thread the store through the evaluation. In particular, we define the order of evaluation
// explicitly through data flow dependencies.

  case If0(cond, thenExp, elseExp)
    => eval(cond, env, s) match {
         case (NumV(0), s1) => eval(thenExp, env, s1)
         case (_, s1)       => eval(elseExp, env, s1)

         /* An alternative that enfoces runtime type-correctness of
          * the conditional expression:
         case (NumV(_), s1) => eval(elseExp, env, s1)
         case _             => sys.error("can only test if a number is 0") */
       }

  case Add(l, r)
    => eval(l, env, s) match {
         case (NumV(v1), s1)
           => eval(r, env, s1) match {
                case (NumV(v2), s2) => (NumV(v1 + v2), s2)
                case _ => sys.error("can only add numbers")
              }
         case _
           => sys.error("can only add numbers")
       }

  case Mul(l, r)
    => eval(l, env, s) match {
         case (NumV(v1), s1)
           => eval(r, env, s1) match {
                case (NumV(v2), s2) => (NumV(v1 * v2), s2)
                case _ => sys.error("can only multiply numbers")
              }
         case _ => sys.error("can only multiply numbers")
       }

  case Ap(f, a)
    => eval(f, env, s) match {
         case (ClosureV(f, closureEnv), s1)
           => eval(a, env, s1) match {
                case (av, s2)
                  => eval(f.body, closureEnv + (f.param -> av), s2)
              }
         case _ => sys.error("can only apply functions")
       }


// In a sequence, we ignore the result of evaluating e1 but not its effect on the store.

  case Seq(e1, e2) => eval(e2, env, eval(e1, env, s)._2)

//  A new box is created by putting it into the store at a new address.

  case NewBox(e: Exp)
    => eval(e, env, s) match {
         case (v, s1) => {
           val a = nextAddress
           (AddressV(a), s1 + (a -> v))
         }
       }

// Setting a box is now a two-step process: First evaluate b to an
// address, then lookup and update the value associated to the
// address in the store. Note that "updated" is a functional method.

  case SetBox(b: Exp, e: Exp)
    => eval(b, env, s) match {
         case (AddressV(a), s1)
           => eval(e, env, s1) match {
                case (ev, s2) => (ev, s2.updated(a, ev))
              }
         case _ => sys.error("can only set boxes")
       }

// OpenBox uses the same two-step process but does not update the
// store.

  case OpenBox(b: Exp)
    => eval(b, env, s) match {
         case (AddressV(a), s1) => (s1(a), s1)
         case _                 => sys.error("can only open boxes")
       }
}

Freeing Memory

From an implementation point of view, our interpreter has the problem that nothing is ever removed from the store. One possibility would be to add an operation "removeBox" or the like to the language, but this would lead to dangling pointers and all the problems associated with manual memory management.

Our model of stores is sufficient to illustrate how modern languages often deal with memory management: by garbage collection. Garbage collectors automatically reclaim memory that is no longer referenced from within the active part of the computation. We can model a (naive) mark-and-sweep garbage collector as follows:

def gc(env: Env, store: Store): Store = {

  def allAddrInVal(v: Value): Set[Address] = v match {
    case AddressV(a)      => Set(a)
    case NumV(_)          => Set.empty
    case ClosureV(f, env) => allAddrInEnv(env)
  }

  def allAddrInEnv(env: Env): Set[Address] =
    env.values.map(allAddrInVal _).fold(Set.empty)(_ union _)

  def mark(seed: Set[Address]): Set[Address] = {
    val newAddresses = seed.flatMap(ad => allAddrInVal(store(ad)))
    if (newAddresses.subsetOf(seed)) seed
    else mark(seed union newAddresses)
  }

  val marked = mark(allAddrInEnv(env)) // mark ...
  store.view.filterKeys(marked(_)).toMap // and sweep!
}
val teststore = Map(
  6  -> NumV(42),
  7  -> NumV(6),
  8  -> AddressV(6),
  9  -> AddressV(7),
  10 -> ClosureV(Fun("x", "y"), Map("y" -> AddressV(8)))
)

/*
10 -> 8 -> 6
      9 -> 7        */

assert(gc(Map("a" -> AddressV(10)), teststore) == teststore - 7 - 9)

Note that garbage collectors only approximate the set of semantically disposable store entities. Even with garbage collectors, applications may very well suffer from memory leaks. The approximation should be safe, in the sense that a datum is never reclaimed when it is used by subsequent computations.

Furthermore, it must reclaim enough garbage to be actually useful. Reachability has turned out to be a rather useful (and sound) approximation of semantic disposability. Garbage collectors must also be efficient. Efficiency of GC is a huge research topic that we are not going to discuss. One efficiency problem with garbage collectors based on reachability that we want to mention is the "stop-the-world" phenomenon.

Garbage Collection

The content of this chapter is available as a Scala file here.

Let us now consider a more accurate modeling of garbage collection (gc). This time, we will use a mutable store instead of a functional store, because our purpose is not to explain mutation but to explain gc.

This is the well-known syntax of our language: FAE with boxes.

enum Exp:
  case Num(n: Int)
  case Id(name: String)
  case Add(lhs: Exp, rhs: Exp)
  case Mul(lhs: Exp, rhs: Exp)
  case If0(cond: Exp, thenExp: Exp, elseExp: Exp)
  case Fun(param: String, body: Exp)
  case Ap (funExpr: Exp, argExpr: Exp)

  case NewBox(e: Exp)
  case SetBox(b: Exp, e: Exp)
  case OpenBox(b: Exp)
  case Seq(e1: Exp, e2: Exp)

object Exp:
  implicit def num2exp(n: Int): Exp = Num(n)
  implicit def id2exp(s: String): Exp = Id(s)
  def wth(x: String, xdef: Exp, body: Exp): Exp = Ap(Fun(x, body), xdef)

import Exp._

We will equip our values with a mutable flag that is useful for mark-and-sweep garbage collection. In real systems it is implemented as a bit flag, or, if the so-called "tri-color algorithm" is used, with two bit flags.

abstract class Value {
  var marked: Boolean = false
}

We will also use a mutable map instead of a map for environments. This is not needed for mark-and-sweep, but for copying garbage collectors such as Cheney's semi-space garbage collection algorithm.

type Env = scala.collection.mutable.Map[String, Value]
case class NumV(n: Int) extends Value
case class ClosureV(f: Fun, env: Env) extends Value
case class AddressV(a: Int) extends Value

To be able to experiment with different store and gc designs, we create an interface for stores. The stack parameter in malloc is needed during gc to determine the root nodes from which the algorithms can start.

trait Store {
  def malloc(stack: List[Env], v: Value): Int
  def update(index: Int, v: Value): Unit
  def apply(index: Int): Value
}

In our interpreter, the stack of environments is only implicitly available on the stack of the meta-language. To reify the call-stack we need to make it explicit. We do so by constructing the stack explicitly and passing it as parameter. The first element of the stack is the current environment; the rest is only needed for gc.

def eval(e: Exp, stack: List[Env], store: Store): Value = e match {

  case Num(n) => NumV(n)

  case Id(x) => stack.head(x)

  case f@Fun(_, _) => ClosureV(f, stack.head)

  /* With a mutable store, we do not have to thread it according to
   * the order of evaluation any more.
   */

  case If0(cond, thenExp, elseExp)
    => eval(cond, stack, store) match {
         case NumV(0) => eval(thenExp, stack, store)
         case _       => eval(elseExp, stack, store)
       }

  /* The mutable store allows us to take advantage of Scala's
   * evaluation order and perform two pattern matchings
   * simultaneously.
   */

  case Add(l, r)
    => (eval(l, stack, store), eval(r, stack, store)) match {
         case (NumV(v1), NumV(v2)) => NumV(v1 + v2)
         case _ => sys.error("can only add numbers")
       }

  case Mul(l, r)
    => (eval(l, stack, store), eval(r, stack, store)) match {
         case (NumV(v1), NumV(v2)) => NumV(v1 * v2)
         case _ => sys.error("can only multiply numbers")
       }

  /* A new environment should be pushed onto the stack only when
   * binding occurs. Where exactly in BCFAE do bindings happen?
   */

  case Ap(f, a)
    => eval(f, stack, store) match {
         case ClosureV(f, cEnv)
           => eval(
                f.body,
                (cEnv += (f.param -> eval(a, stack, store))) :: stack,
                store
              )
         case _ => sys.error("can only apply functions")
       }

  /* The mutable store allows us to implement Seq-expression
   * in terms of sequencing in Scala itself.
   */

  case Seq(e1, e2)
    => eval(e1, stack, store); eval(e2, stack, store)

  case NewBox(e: Exp)
    => {
         val a = store.malloc(stack, eval(e, stack, store))
         AddressV(a)
       }

  case SetBox(b: Exp, e: Exp)
    => eval(b, stack, store) match {
         case AddressV(a)
           => {
                val ev = eval(e, stack, store)
                store.update(a, ev)
                ev
              }
         case _ => sys.error("can only set boxes")
       }

  case OpenBox(b: Exp)
    => eval(b, stack, store) match {
         case AddressV(a) => store(a)
         case _ => sys.error("can only open boxes")
       }
}

Here is one implementation of the Store interface that does not perform gc. It just runs out of memory once the store is full.

class NoGCStore(size: Int) extends Store {

  val memory = new Array[Value](size)

  var nextFreeAddr: Int = 0

  def malloc(stack: List[Env], v: Value): Int = {
    val x = nextFreeAddr
    if (x >= size) sys.error("out of memory")
    nextFreeAddr += 1
    update(x, v)
    x
  }

  def update(index: Int, v: Value): Unit = memory.update(index, v)

  def apply(index: Int) = memory(index)
}

Here is a mark-and-sweep garbage collector.

class MarkAndSweepStore(size: Int) extends Store {

  val memory = new Array[Value](size)

  var free: Int = size

  var nextFreeAddr: Int = 0

  def malloc(stack: List[Env], v: Value): Int = {
    if (free <= 0) gc(stack)
    if (free <= 0) sys.error("out of memory")

    /* Here we find the next available location in memory via a while-
     * loop. In order to avoid maintaining a list of available spaces
     * (because we are lazy), let us assume that no box created in
     * BCFAE can have an address pointing to a null memory cell (which
     * also is the case).
     *
     * If we ensure the invariant that the variable `free` has always
     * the number of free memory space, then the following loop will
     * always halt. The nontermination situation will generate an out-
     * of-memory error and the program will abort. */

    while (memory(nextFreeAddr) != null) {
      nextFreeAddr += 1
      if (nextFreeAddr == size) nextFreeAddr = 0
    }

    free -= 1
    update(nextFreeAddr, v)
    nextFreeAddr
  }

  def update(index: Int, v: Value): Unit = memory.update(index, v)

  def apply(index: Int) = memory(index)

  def allAddrInVal(v: Value): Set[Int] = v match {
    case AddressV(a)      => Set(a)
    case NumV(_)          => Set.empty
    case ClosureV(f, env) => allAddrInEnv(env)
  }

  def allAddrInEnv(env: Env): Set[Int] =
    env.values.map(allAddrInVal _).fold(Set.empty)(_ union _)

  def mark(seed: Set[Int]): Unit = {
    seed.foreach(memory(_).marked = true)
    val newAddresses = seed.flatMap(
                         ad => allAddrInVal(memory(ad))
                       ).filter(!memory(_).marked)
    if(newAddresses != Set.empty) {
      mark(newAddresses)
    }
  }

  /* What graph algorithm underlies the mark step as implemented here?
   * What potential problem could it cause in a "real" interpreter? */

  def sweep(): Unit = {
    memory.indices.foreach(
      index =>   if (memory(index) == null) {
                   /* No work needed on an empty memory cell */
                 }
                 else if (memory(index).marked) {
                   /* Reset `marked` flag for the next gc */
                   memory(index).marked = false
                 }
                 else {
                   free += 1
                   memory(index) = null
                 }
    )
  }

  def gc(stack: List[Env]): Unit = {
    println("\nSTARTING GC\nSTACK = " + stack + "\nSTORE = " + memory)
    mark(stack.map(allAddrInEnv _).fold(Set.empty)(_ union _))
    sweep()
    println("GC COMPLETE\nSTORE = " + memory +
            "\nNUMBER OF FREE SLOTS = " + free)
  }
}
val test4 = wth("makedata", Fun("x", NewBox(NewBox(NewBox("x")))),
                Seq(Ap("makedata", 1),
                Seq(Ap("makedata", 2),
                Seq(wth("s", Ap("makedata", 3),
                            Ap("makedata", "s")),
                    Ap("makedata", 4)))))
def runTest4 = eval(
                 test4,
                 List(scala.collection.mutable.Map.empty),
                 new MarkAndSweepStore(5)
               )

This model of garbage collection does not illustrate the difficulty of memory management. In most languages, the size of the allocated memory regions on the heap vary, and hence one needs an algorithm to find a free and large-enough spot on the heap. There are various algorithms and heuristics (best-fit, worst-fit, first-fit, ...) for that purpose.

There are also various alternative gc designs. Mark-and-sweep is a non-moving algorithm, where reachable heap objects are never moved. In contrast to that, copying gc algorithms move the reachable objects to a different portion of the heap. One of the oldest algorithms is the semi-space garbage collector, in particular with the implementation purpose.

Topic for class discussion: What are the pros and cons of moving vs. non-moving gc?

It can be shown empirically that most unreachable objects become unreachable while they are still young. Generational gc algorithms take this empirical fact into account and divide the objects into generations, whereby the (small) youngest generation of objects is garbage-collected more frequently.

A typical problem of the simple gc algorithms we discussed is the stop-the-world phenomenon: All execution has to be stopped during a gc cycle. This issue is addressed by incremental or concurrent garbage collectors. Incremental garbage collectors typically reduce the total throughput but increase responsiveness and real-time behavior.

A completely different approach to memory management is reference counting. In reference counting, each object on the heap (in our case, each box) maintains a counter which says how many pointers currently point to that object. The counter is adjusted whenever a pointer variable is assigned to this object (incremented), or moved from this object to another object (decremented). When the counter is 0, the object can be reclaimed.

The obvious disadvantage of reference counting is that it cannot detect cycles on the heap. Hence reference counting algorithm must be augmented with some means to detect cycles. Topic for class discussion: What are the pros and cons of reference counting vs. tracing garbage collectors such as mark-and-sweep or semi-space?

Syntactic and Meta Interpretation

The content of this chapter is available as a Scala file here.

For each desired language semantics, there exist many different ways to implement an interpreter in some meta-language to encode this semantics.

One question that is of particular importance is whether a language feature is implemented by using a corresponding language feature of the meta-language, or whether it is implemented using more primitive language constructs. The first case is called meta-interpretation, the second case syntactic interpretation.

Meta-interpretation can be convenient if we are not interested in having control over the exact meaning of the construct, or if the way the meta-language handles this construct is just what we want for our object-language.

Syntactic interpretation is required if we want to understand what the language feature really means in terms of more primitive constructs, or if we want to implement the language feature differently than the meta-language. Of course, if the meta-language has no corresponding feature, then we have no choice but to make a syntactic interpretation.

Our FAE interpreter is a meta-interpreter with respect to many features. For instance, it does not tell us

  • the precision of numbers, or the algorithm for addition
  • how the call stack is managed, e.g. the maximum depth of recursion supported by the interpreter
  • whether/how memory management for closures works (they are objects on the heap!)

That said, it is possible to make the FAE interpreters still more "meta". Here are two examples.

HOAS

The first one is a version of FAE that uses a different representation of the program syntax, namely one using meta-language functions to represent object-language functions. This technique is called higher-order abstract syntax, or HOAS. For instance, the function Fun("x", Add("x", 5)) is now represented as Fun(x => Add(x, 5)). The interpreter becomes rather short, because substitution and lexical scoping are now being dealt with by the corresponding meta-level construct.

object HOAS {
  enum Exp:
    case Num(n: Int)
    case Id(name: String)
    case Add(lhs: Exp, rhs: Exp)
    case Fun(f: Exp => Exp)
    case Ap(funExpr: Exp, argExpr: Exp)

  import Exp._

  def eval(e: Exp): Exp = e match {
      case Id(v) => sys.error("unbound identifier: " + v)
      case Add(l, r) => (eval(l), eval(r)) match {
                         case (Num(x), Num(y)) => Num(x + y)
                         case _ => sys.error("can only add numbers")
                        }
      case Ap(f, a) => eval(f) match {
         case Fun(f) => eval(f(eval(a)))
         case _ => sys.error("can only apply functions")
      }
      case _ => e // numbers and functions evaluate to themselves
    }
}

Meta-Level Closures

A different way to use meta-level functions in the interpreter is to represent object-level closures by meta-level closures. Notice that this interpreter then has no control anymore about scoping; rather, it is completely inherited from the meta-language.

A particularly pleasing and important property of this interpreter below is that it is compositional, meaning that all recursive calls of eval are only on subparts of the original expression. This means that it becomes particularly easy to reason about program equivalence in the object-language in terms of program equivalence in the meta-language: Two object-language expressions are equivalent if their "denotations" as meta-level expressions are equivalent in the meta-level.

Compositionality is the cornerstone of denotational semantics. A denotational semantics can be thought of as a compositional interpreter in which the meta-language is mathematics. Compositionality also has a rather practical side-effect: It means that we can implement the interpreter in the internal visitor style that we learned about at the beginning of the course (recall that the internal visitor style enforces compositionality). Recommended exercise: Re-implement the interpreter as an internal visitor.

enum Exp:
  case Num(n: Int)
  case Id(name: String)
  case Add(lhs: Exp, rhs: Exp)
  case Fun(param: String, body: Exp)
  case Ap(funExpr: Exp, argExpr: Exp)

import Exp._

object Compositional {
    sealed abstract class Value
    type Env = Map[String, Value]
    case class NumV(n: Int) extends Value
    case class FunV(f: Value => Value) extends Value

    def eval(e: Exp): Env => Value = e match {
      case Num(n: Int) => (env) => NumV(n)
      case Id(x) => env => env(x)
      case Add(l, r) => { (env) =>
        (eval(l)(env),  eval(r)(env)) match {
          case (NumV(v1), NumV(v2)) => NumV(v1 + v2)
          case _ => sys.error("can only add numbers")
        }
      }
      case Fun(param, body) => (env) => FunV((v) => eval(body)(env + (param -> v)))
      case Ap(f, a) => (env) => (eval(f)(env), eval(a)(env)) match {
        // Use environment stored in (meta-level) closure to realize proper lexical scoping!
        case (FunV(g), arg) => g(arg)
        case _ => sys.error("can only apply functions")
      }
    }
}

For comparison, here is our original FAE interpreter.

object FAE {
    sealed abstract class Value
    type Env = Map[String, Value]
    case class NumV(n: Int) extends Value
    case class ClosureV(f: Fun, env: Env) extends Value

    def eval(e: Exp, env: Env): Value = e match {
      case Num(n: Int) => NumV(n)
      case Id(x) => env(x)
      case Add(l, r) => {
        (eval(l, env), eval(r, env)) match {
          case (NumV(v1), NumV(v2)) => NumV(v1 + v2)
          case _ => sys.error("can only add numbers")
        }
      }
      case f@Fun(param, body) => ClosureV(f, env)
      case Ap(f, a) => eval(f, env) match {
        // Use environment stored in closure to realize proper lexical scoping!
        case ClosureV(f, closureEnv) => eval(f.body, closureEnv + (f.param -> eval(a, env)))
        case _ => sys.error("can only apply functions")
      }
    }
}

We will soon learn about ways to make FAE more syntactic in various ways. For instance, we will no longer rely on call-stack management of the meta-language, or the existence of higher-order functions.

One dimension in which the interpreter could easily be made more syntactic is the treatment of numbers and arithmetic. For instance, we could represent numbers as sequences of digits instead of Scala numbers. Another aspect in which our FAE interpreter relies on the host language is memory management.

This is particularly relevant for environments stored inside closures. These environments cannot be organized on the call stack and hence need memory management. Since we are using Scala references to refer to environments, environments that are no longer needed are collected by the Scala (or rather, Java) virtual machine.

Church Encodings

The content of this chapter is available as a Scala file here.

Today we shrink our language. It does not seem to be big, but today we want to illustrate how powerful our core language, the lambda calculus, is. Here is a shrinked version of FAE that does not even have numbers anymore. For testing purposes, we introduce a new expression PrintDot whose semantics is to print a dot on the screen.

enum Exp:
  case Id(name: String)
  case Fun(param: String, body: Exp)
  case Ap(funExpr: Exp, argExpr: Exp)
  case PrintDot()

object Exp:
  implicit def id2exp(s: String): Exp = Id(s)

import Exp._

abstract class Value
type Env = Map[String, Value]
// the only values are closures
case class ClosureV(f: Fun, env: Env) extends Value

Notice that the only values in this language are closures. This means that there cannot be the situation anymore that we expect, say, a number but get in fact a closure. Hence, this language has the fascinating property that no dynamic type errors can occur.

def eval(e: Exp, env: Env): Value = e match {
  // We give the identity function as dummy value for PrintDot
  case PrintDot() => print("."); ClosureV(Fun("x", "x"), Map.empty)
  case Id(x) => env(x)
  case f@Fun(param, body) => ClosureV(f, env)
  case Ap(f, a) => eval(f, env) match {
    case ClosureV(f, closureEnv) => eval(f.body, closureEnv + (f.param -> eval(a, env)))
  }
}

Now we want to illustrate that we can, in principle, bootstrap a full programming language from this small core. To do so, we use the technique of Church encoding. This means that each datum is represented by its own fold function.

Church Encoding of Booleans

Let's start with booleans and boolean arithmetic.

val f = Fun("t", Fun("f", "f"))  // false
val t = Fun("t", Fun("f", "t"))  // true
val and = Fun("a", Fun("b", Ap(Ap("a", "b"), "a")))
val or = Fun("a", Fun("b", Ap(Ap("a", "a"), "b")))
val not = Fun("a", Fun("t", Fun("f", Ap(Ap("a", "f"), "t"))))

We can now write our own control structures, such as if/then/else

val ifthenelse = Fun("cond", Fun("t", Fun("f", Ap(Ap("cond", "t"), "f"))))

Church Encoding of Numbers

Let's now consider Numbers. We encode them as Peano numbers. These encodings of numbers are often called Church numbers.

val zero = Fun("s", Fun("z", "z"))
val succ = Fun("n", Fun("s", Fun("z", Ap("s", Ap(Ap("n", "s"), "z")))))
val one = Ap(succ, zero)
val two = Ap(succ, one)
val three = Ap(succ, two)
val add  = Fun("a", Fun("b", Fun("s", Fun("z", Ap(Ap("a", "s"), Ap(Ap("b", "s"), "z"))))))
val mult = Fun("a", Fun("b", Fun("s", Fun("z", Ap(Ap("a", Ap("b", "s")), "z")))))
val exp  = Fun("a", Fun("b", Ap(Ap("b", Ap(mult, "a")), one)))
val iszero = Fun("a", Ap(Ap("a", Fun("x", f)), t))

The predecessor function is more complicated (why?). We do not show it here. For testing, here is a function that prints a unary representation of a number.

val printnum = Fun("a", Ap(Ap("a", Fun("x", PrintDot())), f))

Church encoding of lists

Again straightforward, except "tail", which we do not show here. It needs the same kind of trick (called "pairing trick") as the predecessor function.

val emptylist = Fun("c", Fun("e", "e"))
val cons = Fun("h", Fun("r", Fun("c", Fun("e", Ap(Ap("c", "h"), Ap(Ap("r", "c"), "e"))))))
val head = Fun("l", Ap(Ap("l", Fun("h", Fun("t", "h"))), f))

For instance, we can multiply all numbers in a list

val multlist = Fun("l", Ap(Ap("l", mult), one))

Here is the list 3, 2, 3:

val list323 = Ap(Ap(cons, three), Ap(Ap(cons, two), Ap(Ap(cons, three), emptylist)))

val test = Ap(printnum, Ap(multlist, list323))

Calling exec should yield 18 dots before the dummy result

val exec = eval(test, Map.empty)
// ..................
// exec: Value = ClosureV(
//   f = Fun(param = "x", body = Id(name = "x")),
//   env = Map()
// )

Topic for class discussion: Can we do these encodings directly in Scala or Haskell?

Object Algebras

The content of this chapter is available as a Scala file here.

Object algebras: A practical way of using Church Encodings

In previous lectures, we have talked about the "expression problem": The problem of encoding a data structure (like expressions) and functions on that data structure (like evaluators, or pretty-printers) in such a way that both the data structure and the functions thereon can be extended without modifying code, while at the same time maintaining static type safety.

Church encodings look like an obscure theoretical idea at first, but we will see that their incarnation as "object algebras" leads to a quite attractive and practical solution to the expression problem.

Let's look at a Scala version of the Church encodings we saw for the lambda calculus. We use objects instead of functions, but otherwise it is the same. Let's start with booleans.

trait Bool {
  def ifthenelse[T](t: T, e: T): T
}

case object T extends Bool {
  def ifthenelse[T](t: T, e: T) = t
}
case object F extends Bool {
  def ifthenelse[T](t: T, e: T) = e
}
def and(a: Bool, b: Bool): Bool = a.ifthenelse(b, a)

In Smalltalk and related languages, booleans are actually implemented like that (at least conceptually).

In the same way, we can encode Church numerals.

trait NumC {
  def fold[T](z: T, s: T => T): T
}
case object Zero extends NumC {
  def fold[T](z: T, s: T => T) = z
}

case class Succ(n: NumC) extends NumC {
  def fold[T](z: T, s: T => T) = s(n.fold(z, s))
}

def plus(a: NumC, b: NumC) = {
  new NumC {
    def fold[T](z: T, s: T => T): T = {
      a.fold(b.fold(z, s), s)
    }
  }
}
val oneC = Succ(Zero)
val twoC = Succ(oneC)
val threeC = Succ(twoC)

def testplusC = plus(twoC, threeC).fold[Unit]((), _ => print("."))

Object algebras are a different way to do Church encodings in object-oriented languages. This is what the encoding of Church numbers looks like in object-algebra style.

trait NumSig[T] {
  def z: T
  def s(p: T): T
}

trait Num {
  def apply[T](x: NumSig[T]): T
}

In other words, every constructor of the data type is turned into a function of the NumSig type, with recursive occurences being replaced by the type constructor T. Actual numbers have the type Num, i.e., they are basically functions of type NumSig[T] => T.

In the terminology of universal algebra, NumSig is an algebraic signature, and NumSig[T] => T is the type of algebras for that signature. Compared to the Church encoding above, we bundle the arguments of the fold-function into a trait NumSig and pass them as a single object. The advantage of this encoding is that we can extend the set of parameters of the fold by using inheritance.

In this encoding, the plus-function looks like this:

def plus(a: Num, b: Num) = new Num {
  def apply[T](x: NumSig[T]): T = a(new NumSig[T] {
    def z = b(x)
    def s(p: T) = x.s(p)
  })
}

Here is the representation of some numbers.

val zero: Num = new Num { def apply[T](x: NumSig[T]) = x.z }
val one: Num = new Num { def apply[T](x: NumSig[T]) = x.s(x.z) }
val two: Num = new Num { def apply[T](x: NumSig[T]) = x.s(one(x)) }
val three: Num = new Num { def apply[T](x: NumSig[T]) = x.s(two(x)) }

This is an interpretation of the Num-"language" as Scala integers:

object NumAlg extends NumSig[Int] {
  def z = 0
  def s(x: Int) = x + 1
}
val testplus = plus(two, three)(NumAlg)
// testplus: Int = 5

Let's look at a more useful application of object algebras. We encode expression trees as object algebras.

trait Exp[T] {
  implicit def id(name: String): T
  def fun(param: String, body: T): T
  def ap(funExpr: T, argExpr: T): T
  implicit def num(n: Int): T
  def add(e1: T, e2: T): T
  def wth(x: String, xdef: T, body: T): T = ap(fun(x, body), xdef)
}

The structure of expressions forces compositional interpretations. Hence we use the compositional interpretation using meta-level closures to represent closures.

sealed abstract class Value
type Env = Map[String, Value]
case class ClosureV(f: Value => Value) extends Value
case class NumV(n: Int) extends Value

An interpretation of expressions is now an implementation of the Exp interface

trait eval extends Exp[Env => Value] {
  def id(name: String) = env => env(name)
  def fun(param: String, body: Env => Value) = env => ClosureV(v => body(env + (param -> v)))
  def ap(funExpr: Env => Value, argExpr: Env => Value) = env => funExpr(env) match {
    case ClosureV(f) => f(argExpr(env))
    case _ => sys.error("can only apply functions")
  }
  def num(n: Int) = env => NumV(n)
  def add(e1: Env => Value, e2: Env => Value) = env => (e1(env), e2(env)) match {
    case (NumV(n1), NumV(n2)) => NumV(n1 + n2)
    case _ => sys.error("can only add numbers")
  }
}

object eval extends eval

An example program becomes a function that is parametric in the chosen interpretation:

def test[T](semantics: Exp[T]) = {
  import semantics._

  ap(ap(fun("x", fun("y", add("x", "y"))), 5), 3)
}

We evaluate the program by folding the eval-visitor over it.

val testres = test(eval)(Map.empty)
// testres: Value = NumV(n = 8)

The object algebra encoding of expressions is quite extensible. For instance, we can add another case to the expression data type by extending the interface.

trait ExpWithMult[T] extends Exp[T] {
  def mult(e1: T, e2: T): T
}

trait evalWithMult extends eval with ExpWithMult[Env => Value] {
  def mult(e1: Env => Value, e2: Env => Value) = env => (e1(env), e2(env)) match {
    case (NumV(n1), NumV(n2)) => NumV(n1 * n2)
    case _ => sys.error("can only multiply numbers")
  }
}
object evalWithMult extends evalWithMult

def testMult[T](semantics: ExpWithMult[T]) = {
  import semantics._

  ap(ap(fun("x", fun("y", mult("x", "y"))), 5), 3)
}
val testresMult = testMult(evalWithMult)(Map.empty)
// testresMult: Value = NumV(n = 15)

Note that there is no danger of confusing the language variants. For instance, an attempt to pass testMult to eval will be a static type error.

At the same time, we can add another function on expressions (such as a pretty-printer) without changing existing code, too: Just add a trait prettyPrint extends Exp[String] and, if pretty-printing of ExpWithMult is required, extend with trait prettyPrintMult extends prettyPrint with ExpWithMult[String]. This is the object algebra way of solving the expression problem.

We can also go one step further and combine object algebras with typed higher-order abstract syntax, using higher-kinded type members. Don't panic if you don't understand what is going on here.

trait ExpT {
  type Rep[_]
  def fun[S, T](f: Rep[S] => Rep[T]): Rep[S => T]
  def ap[S, T](funExpr: Rep[S => T], argExpr: Rep[S]): Rep[T]
  implicit def num(n: Int): Rep[Int]
  def add(e1: Rep[Int], e2: Rep[Int]): Rep[Int]
}

Instead of having the semantic domain as a type parameter T as above, we encode it as a higher-kinded abstract type member Rep[_]. This leads to significantly shorter type signatures and more fine-grained typing via Scala's support for "path-dependent types".

Note that, in contrast to eval, no dynamic checks (match ...) are needed in the interpreter. Also, the result type of the evaluator is just X. In contrast to Value, this is a "tagless" type, that is, it does not maintain information at runtime about which variant it is. This is because the ExpT datatype guarantees well-typedness of expressions. This interpreter will, in general, run much faster and can in fact be formulated in such a way that interpreting a program is just as fast as writing the program directly in the meta language.

object evalT extends ExpT {
  type Rep[X] = X
  def fun[S, T](f: S => T) = f
  def ap[S, T](f: S => T, a: S) = f(a)
  def num(n: Int) = n
  def add(e1: Int, e2: Int) = e1 + e2
}

object prettyprintT extends ExpT {
  var counter = 0
  type Rep[X] = String
  def fun[S, T](f: String => String) = {
    val varname = "x" + counter.toString
    counter += 1
    "(" + varname + " => " +  f(varname) + ")"
  }
  def ap[S, T](f: String, a: String) = f + "(" + a + ")"
  def num(n: Int) = n.toString
  def add(e1: String, e2: String) = "(" + e1 + "+" + e2 + ")"
}

def test2(semantics: ExpT) = {
  import semantics._
  ap(ap(fun((x: Rep[Int]) => fun((y: Rep[Int]) => add(x, y))), 5), 3)
}
val testres2 = test2(evalT)
// testres2: Int = 8
val testres3 = test2(prettyprintT)
// testres3: String = "(x0 => (x1 => (x0+x1)))(5)(3)"

An attempt to construct an ill-formed object program will be detected by the Scala type checker. For instance, the following program which adds a number and a function is rejected by the Scala type checker:

def testilltyped(semantics: ExpT) = {
  import semantics._
  add(5, fun((x: Rep[Int]) => x))
}
// error:
// Found:    semantics.Rep[Int => Int]
// Required: semantics.Rep[Int]
//   add(5, fun((x: Rep[Int]) => x))
//          ^^^^^^^^^^^^^^^^^^^^^^^

The type system encoded in the ExpT type is the so-called "simply-typed lambda calculus". Encoding more expressive type system in a similar style (including, e.g., type parameters) is an active area of research.

References: B. Olivira, W.Cook. Extensibility for the Masses: Practical Extensibility with Object Algebras. Proc. ECOOP 2012.

Continuations: Motivation

The content of this chapter is available as a Scala file here.

Consider the following very simple program:

def inputNumber(prompt: String): Int = {
  println(prompt)
  Integer.parseInt(readLine())
}
def progSimple = {
  println(inputNumber("First number:") + inputNumber("Second number"))
}

Now consider a web version of the program:

  • the first number is entered on the first page
  • then the first number is submitted, a form to enter the second number is shown
  • when the second number is submitted, a form with the result is generated and shown.

Even this "addition server" is difficult to implement. Because http is a stateless protocol (for good reasons), every web program is basically forced to terminate after every request. This means that the Web developer must turn this application into three programs:

  1. The first program displays the first form.
  2. The second program consumes the form values from the first form, and generates the second form.
  3. The third program consumes the form values from the second form, computes the output, and generates the result.

Because the value entered in the first form is needed by the third program to compute its output, this value must somehow be transmitted from the first program to the third. This is typically done by using the hidden field mechanism of HTML.

Suppose, instead of using a hidden field, the application developer used a Java Servlet session object, or a database field, to store the first number. (Application developers are often pushed to do this because that is the feature most conveniently supported by the language and API they are employing.) Then, if the developer were to exploratorily open multiple windows the application can compute the wrong answer. Hence, the actual program in a web developer's mind has the structure of the following program. An actual web program is of course more complicated, but this primitive model of web programming is sufficient to explain the problem.

// we use the return type "Nothing" for functions that will never return (normally)
def webdisplay(s: String): Nothing = {
  println(s)
  sys.error("program terminated")
}

def webread(prompt: String, continue: String): Nothing = {
  println(prompt)
  println("send input to: " + continue)
  sys.error("program terminated")
}

def program1 = webread("enter first number", "s2")
def program2(n1: Int) = webread("enter second number and remind me of previoulsy entered number " + n1, "s3")
def program3(n1: Int, n2: Int) = webdisplay("The sum of " + n1 + " and " + n2 + " is " + (n1 + n2))

We could write a better webread and webdisplay procedure if we could somehow get hold of the pending computation at the time when the procedure was invoked. Such a pending computation is called continuation.

Let's consider the continuation at the point of the first interaction in the original program,

def prog = {
  println(inputNumber() + inputNumber())
}

The pending computation (or, continuation in the following) is to take the result of the first inputNumber invocation, add to it the result of the second invocation, and display it.

We can express the continuation as a function:

val cont1 = (n: Int) => println(n + inputNumber("Second number"))

Similarly, the continuation at the second point of interaction is:

val cont2 = (m: Int) => println(n + m)

where n is the result of the first input, which is stored in the closure of cont2.

Assuming an explicit representation of the continuation, it is obvious that we can now write a version of webread, called webread_k, which takes the continuation as second parameter and invokes the continuation once the answer to the result is received by the server. This can be implemented, say, by associating to a continuation a unique ID, store the continuation in a hashmap on the server using this ID, and storing the ID in the form such that it gets submitted back to the server once the client presses the submit button.

Here is code illustrating the idea. For simplicity we assume that all web forms return a single integer value.

val continuations = new scala.collection.mutable.HashMap[String, Int => Nothing]()
var nextIndex: Int = 0
def getNextID = {
  nextIndex += 1
  "c" + nextIndex
}

def webread_k(prompt: String, k: Int => Nothing): Nothing = {
  val id = getNextID
  continuations += (id -> k)
  println(prompt)
  println("to continue, invoke continuation: " + id)
  sys.error("program terminated")
}

def continue(kid: String, result: Int) = continuations(kid)(result)

Using webread_k, we can now define our addition server as follows. If you try webprog, ignore the stack traces.

def webprog = webread_k("enter first number", (n) =>
                webread_k("enter second number", (m) =>
                  webdisplay("The sum of " + n + " and " + m + " is " + (n + m))))

For instance, try:

scala> webprog            -- yields some continuation id "c1"
scala> continue("c1", 5)  -- yields some continuation id "c2"
scala> continue("c2", 7)

This should yield the result 12 as expected. But also try:

scala> webprog            -- yields some continuation id "c1"
scala> continue("c1", 5)  -- yields some continuation id "c2"
scala> continue("c1", 6)  -- yields some continuation id "c3"
scala> continue("c2", 3)  -- should yield 8
scala> continue("c3", 3)  -- should yield 9

The style of programming in webprog, which is obviously more complicated than the logical structure of progSimple, shows up in many practical programming scenarios - server-side web programming is just one of them. For instance, in JavaScript many API functions for asynchronous communication such as httprequest in AJAX require to pass a callback function, which leads to similar code as the one in webprog above.

Let's consider this "web transformation" - the transformation from progSimple to webprog - in more detail. To this end, let's make our application a bit more sophisticated. Instead of entering only two numbers, the user enters n numbers, e.g., the prices of a list of n items.

Here is the "non-web" version of the application:

def allCosts(itemList: List[String]): Int = itemList match {
   case Nil => 0
   case x :: rest => inputNumber("Cost of item " + x + ":") + allCosts(rest)
}
val testData: List[String] = List("banana", "apple", "orange")

def test = println("Total sum: " + allCosts(testData))

This version of allCosts is clearly not web-friendly, because it uses inputNumber, which we do not know how to implement for the web.

The first thing to observe is that on its own, allCosts is not a complete program: it doesn't do anything unless it is called with actual arguments! Instead, it is a library procedure that may be used in many different contexts. Because it has a web interaction, however, there is the danger that at the point of interaction, the rest of the computation -- that is, the computation that invoked allCosts -- will be lost. To prevent this, allCosts must consume an extra argument, a continuation, that represents the rest of the pending computation. To signify this change in contract, we will use the convention of appending _k to the name of the procedure and k to name the continuation parameter.

Here is a first attempt to define allCosts_k.

 def allCosts_k(itemList: List[String], k: Int => Nothing): Nothing = itemList match {
   case Nil => 0
   case x :: rest => webread_k("Cost of item " + x + ":", n => n + allCosts_k(rest, k))
}

This may look reasonable, but it suffers from an immediate problem. When the recursive call occurs, if the list had two or more elements, then there will immediately be another web interaction. Because this will terminate the program, the pending addition will be lost! Therefore, the addition of n has to move into the continuation fed to allCosts_k. In code:

def allCosts_k(itemList: List[String], k: Int => Nothing): Nothing = itemList match {
   case Nil => 0
   case x :: rest => webread_k("Cost of item " + x + ":", n => allCosts_k(rest, m => k(m + n)))
}

That is, the receiver of the web interaction is invoked with the cost of the first item. When allCosts_k is invoked recursively, it is applied to the rest of the list. Its receiver must therefore receive the tally of costs of the remaining items. That explains the pattern in the receiver.

The only problem is, where does a continuation ever get a value? We create larger-and-larger continuations on each recursive invocation, but what ever invokes them?

Here is the same problem from a different angle (that also answers the question above). Notice that each recursive invocation of allCosts_k takes place in the aftermath of a web interaction. We have already seen how the act of web interaction terminates the pending computation. Therefore, when the list empties, where is the value 0 going? Presumably to the pending computation, but there is none. Any computation that would have been pending has now been recorded in k, which is expecting a value. Therefore, the correct transformation of this procedure is:

def allCosts_k(itemList: List[String], k: Int => Nothing): Nothing = itemList match {
   case Nil => k(0)
   case x :: rest => webread_k("Cost of item " + x + ":", n => allCosts_k(rest, m => k(m + n)))
}

def testweb = allCosts_k(testData, m => webdisplay("Total sum: " + m))

Let's now consider the case that we have used a library function for the iteration in allCosts, namely the map function, which we replicate here.

def map[S, T](c: List[S], f: S => T): List[T] = c match {
  case Nil => Nil
  case x :: rest => f(x) :: map(rest, f)
}

Using map, we can rephrase allCosts as follows.

def allCosts2(itemList: List[String]): Int =
  map(itemList, (x: String) => inputNumber("Cost of item " + x + ":")).sum

What if we we want to use map in allCosts_k?

Just using webread_k in place of inputNumber will not work, since webread_k expects an additional parameter. Obviously we must somehow modify the definition of map. But what can we pass as second parameter in the call to f?

Insight: We must perform the "web transformation" to map as well. We call the result map_k. Here is the code:

def map_k[S, T](c: List[S], f: (S, T => Nothing) => Nothing, k: List[T] => Nothing): Nothing = c match {
   case Nil => k(Nil)
   case x :: rest => f(x, t => map_k(rest, f, (tr: List[T]) => k(t :: tr)))
}

def allCosts2_k(itemList: List[String], k: Int => Nothing): Nothing =
   map_k(itemList,
         (x: String, k2: Int => Nothing) =>
           webread_k("Cost of item " + x + ":", k2),
         (l: List[Int]) => k(l.sum))

Implications of the "web transformation":

  1. We had to make decisions about the order of evaluation. That is, we had to choose whether to evaluate the left or the right argument of addition first. This was an issue we had specified only implicitly earlier; if our evaluator had chosen to evaluate arguments right-to-left, the web program at the beginning of this document would have asked for the second argument before the first! We have made this left-to-right order of evaluation explicit in our transformation.

  2. The transformation we use is global, namely it (potentially) affects all the procedures in the program by forcing them all to consume an extra continuation as an argument. We usually don't have a choice as to whether or not to transform a procedure. Suppose f invokes g and g invokes h, and we transform f to f_k but don't transform g or h. Now when f_k invokes g and g invokes h, suppose h consumes input from the web. At this point the program terminates, but the last continuation procedure (necessary to resume the computation when the user supplies an input) is the one given to f_k, with all record of g and h lost.

  3. This transformation sequentializes the program. Given a nested expression, it forces the programmer to choose which sub-expression to evaluate first (a consequence of the first point above); further, every subsequent operation lies in the continuation, which in turn picks the first expression to evaluate, pushing all other operations into its continuation; and so forth. The net result is a program that looks an awful lot like a traditional procedural program. This suggests that this series of transformations can be used to compile a program in a language like Scheme into one in a language like C!

Continuations: Definition

The content of this chapter is available as a Scala file here.

Today's goal is to make the "web" (or rather, CPS) transformation which we applied informally in the previous lecture formal.

In the previous lecture we have seen that we had to translate the following program:

println("The sum is: " + (inputNumber("First number:") + inputNumber("Second number")))

into this program:

webread_k("First number", (n) =>
  webread_k("Second number:", (m) =>
    webdisplay("The sum is: " + (n + m))))

This hand-translation is sufficient if this expression is the entire program. If we wish to use it as a sub-expression in a larger program, this does not suffice, because there may be a pending computation outside its own evaluation. For that reason, the program has to send its result to a continuation instead of returning it:

k => webread_k("First number", (n) =>
       webread_k("Second number:", (m) =>
         webdisplay("The sum is: " + k(n + m))))

This version can be employed in the transformation of a larger program. In the special case where this is the entire program we can apply the transformed term to the identity function to get the same result as the previous manual transformation.

In general, every term, when converted to CPS, will be have the following properties (see O. Danvy, Three Steps for the CPS Transformation, 1991):

  1. The values of all intermediate applications are given a name.

  2. The evaluation of these applications is sequentialized based on a traversal of their abstract syntax tree.

  3. The results of the transformation are procedures that expect a continuation parameter - a lambda abstraction whose application to intermediate values yields the final result of the whole evaluation.

Function application

Let us now look at the transformation of a function application. For instance, let us consider the term

f(a)(g(b))

The transformation of the function argument, f(a), should be

f_k(a, fval => ...)

Similarly, the transformation of the argument position would be:

g_k(b, arg => ...)

Given these two values, fval and arg, we can now perform the application, like so:

k(fval(arg))

However, this will not work, because if fval is in CPS itself, it will not return. Instead, k must be given as an argument to the function, like so:

k => f_k(a, fval => g_k(b, arg => fval(arg, k)))

Reading this sequentially, it says to evaluate the function expression, store its value in fval, then evaluate the argument, store its value in arg, and finally invoke the function value on the argument. This function's continuation is the outer continuation of the function application itself.

Variables and constants

What about variables and constants? Since every term in CPS must be a function that consumes a continuation, the constant is simply sent to the continuation. For instance, the CPS transformation of the number 3 is k => k(3).

Function definitions

What about function definitions, such as x => x? Since every lambda expression is also a constant, we might be tempted to use the same rule as above, i.e.,

k => k(x => x)

However, the transformation is more subtle. A function application invokes the function on two arguments, whereas the original function x => x consumes only one. What is the second argument?

Answer: It is the dynamic continuation, i.e., the continuation at the time of the function application (as opposed to its definition). We cannot ignore this continuation: It is the stack active at the point of function invocation, so we want to preserve it. This is in contrast to what we did with environments, and more in line with our treatment of the store.

The transformed version hence reads:

k => k((x, dynk) => (k => k(x))(dynk))

which is equivalent (when the inner application finally happens) to:

k => k((x, dynk) => dynk(x))

This is a function that accepts a value and a dynamic continuation and sends the value to that continuation.

Formal Definition of the CPS Transformation

We are now ready to write this transformation formally, as a source-to-source transformation. This transformation could have the type cps(e: Exp): Exp, but we choose a different type for two reasons:

  1. In CPS we need function definitions and applications with two arguments instead of one. This could be addressed by adding new syntax.

  2. More importantly, we want the invariants of the CPS format to be clearly visible in the syntax definition of the result, most importantly the fact that all function applications are tail calls.

enum Exp:
  case Num(n: Int)
  case Id(name: String)
  case Add(lhs: Exp, rhs: Exp)
  case Fun(param: String, body: Exp)
  case Ap(funExpr: Exp, argExpr: Exp)

object Exp:
  implicit def num2exp(n: Int): Exp = Num(n)
  implicit def id2exp(s: String): Exp = Id(s)

For CPS transformed terms, we define two different syntactic categories: Serious expressions (SeriousExp) and trivial expressions (TrivExp). By "trivial", we mean terms that "return" and have no control effect - in our language these are terms that are different from applications of functions or continuations (neither of which returns). Additions (CPSAdd) are also values in this regard: We assume addition to be built-in and not requiring CPS-transformations.

The terminology of serious and trivial expressions was introduced by John C. Reynolds in his seminal 1972 paper on "Definitional interpreters for higher-order programming languages".

The syntax makes clear that all arguments of a function application are trivial - hence no nesting of applications can occur. Furthermore, the syntax differentiates between defining an ordinary function (CPSFun) which, when translated, gets an additional continuation parameter, and Continuation Functions (CPSCont), which are the result of the CPS transformation. Correspondingly, we have two different forms of applications, CPSContAp and CPSFunAp.

Here is the formal definition:

sealed abstract class SeriousExp
abstract class TrivExp extends SeriousExp
case class CPSNum(n: Int) extends TrivExp
case class CPSAdd(l: CPSVar, r: CPSVar) extends TrivExp
case class CPSCont(v: String, body: SeriousExp) extends TrivExp
case class CPSFun(x: String, k: String, body: SeriousExp) extends TrivExp
case class CPSVar(x: String) extends TrivExp { override def toString = x.toString }
implicit def id2cpsexp(x: String): CPSVar = CPSVar(x)

case class CPSContAp(k: TrivExp, a: TrivExp) extends SeriousExp
// the arguments are even CPSVar and not only TrivExp!
case class CPSFunAp(f: CPSVar, a: CPSVar, k: CPSVar) extends SeriousExp

With these definitions, we are now ready to formalize the transformation described above. There is one technical issue: We need to introduce new names for binders into our program, such as "k". We need to make sure that we do not accidentially capture existing names in the program. For this reason we need our freshName machinery we introduced for FAE.

def freeVars(e: Exp): Set[String] =  e match {
   case Id(x) => Set(x)
   case Add(l, r) => freeVars(l) ++ freeVars(r)
   case Fun(x, body) => freeVars(body) - x
   case Ap(f, a) => freeVars(f) ++ freeVars(a)
   case Num(n) => Set.empty
}

def freshName(names: Set[String], default: String): String = {
  var last: Int = 0
  var freshName = default
  while (names contains freshName) { freshName = default + last; last += 1; }
  freshName
}

def cps(e: Exp): CPSCont = e match {
   case Add(e1, e2) => {
     val k = freshName(freeVars(e), "k")
     val lv = freshName(freeVars(e2), "lv")
     CPSCont(k, CPSContAp(cps(e1), CPSCont(lv,
       CPSContAp(cps(e2), CPSCont("rv",
         CPSContAp(k, CPSAdd("rv", lv)))))))
   }
   case Fun(a, body) => {
     val k = freshName(freeVars(e), "k")
     val dynk = freshName(freeVars(e), "dynk")
     CPSCont(k, CPSContAp(k, CPSFun(a, dynk, CPSContAp(cps(body), dynk))))
   }
   case Ap(f, a) => {
     val k = freshName(freeVars(e), "k")
     val fval = freshName(freeVars(a), "fval")
     CPSCont(k, CPSContAp(cps(f), CPSCont(fval,
       CPSContAp(cps(a), CPSCont("aval",
         CPSFunAp(fval, "aval", k))))))
   }
   case Id(x) => {
     val k = freshName(freeVars(e), "k")
     CPSCont(k, CPSContAp(k, CPSVar(x)))
   }
   case Num(n) => {
     val k = freshName(freeVars(e), "k")
     CPSCont(k, CPSContAp("k", CPSNum(n)))
   }
}

This transformation is the so-called Fischer CPS transformation. There are many other CPS transformation algorithms. The Fischer CPS transformation is nice because it is so simple and because it is defined as one simple structural recursion over the AST. Its main disadvantage is the existence of so-called "administrative redexes".

An administrative redex is a function application whose operator is a "continuation lambda" - a lambda produced during CPS transformation that was not in the original program. Such function applications can be computed immediately because the function which is called is known.

For instance, cps(Add(2, 3)) yields

CPSCont("k",
        CPSContAp(
          CPSCont("k",
                  CPSContAp("k", 2)),
          CPSCont("lv",
                  CPSContAp(
                    CPSCont("k",
                            CPSContAp("k", 3)),
                    CPSCont("rv",
                            CPSContAp("k", CPSAdd("rv", "lv")))))))

instead of

CPSCont("k", CPSContAp("k", CPSAdd(2, 3)))

Many more advanced CPS transformation algorithms try to avoid as many administrative redexes as possible.

First-Class Continuations

The content of this chapter is available as a Scala file here.

Today's goal is to formalize first-class continuations as illustrated by Scheme's let/cc construct. In the previous lecture we have learned why first class continuations are a powerful language construct. Today we learn the semantics of first-class continuations by extending our interpreter to support let/cc. Here is the abstract syntax of the language extended with Letcc:

enum Exp:
  case Num(n: Int)
  case Id(name: String)
  case Add(lhs: Exp, rhs: Exp)
  case Fun(param: String, body: Exp)
  case Ap(funExpr: Exp, argExpr: Exp)
  /** The abstract syntax of Letcc is as follows */
  case Letcc(param: String, body: Exp)

object Exp:
  implicit def num2exp(n: Int): Exp = Num(n)
  implicit def id2exp(s: String): Exp = Id(s)

But how do we implement Letcc? How do we get hold of the rest of the computation of the object (=interpreted) program? One idea would be to CPS-transform the object program. Then we would have the current continuation available and could store it in environments etc. However, we want to give a direct semantics to first-class continuations, without first transforming the object program.

Insight: If we CPS-transform the interpreter, the continuation of the interpreter also represents, in some way, the continuation of the object program. The difference is that it represents what's left to do in the interpreter and not in the object program. However, what is left in the interpreter is what is left in the object program.

Hence, we are faced with two tasks:

  1. CPS-transform the interpreter
  2. add a branch for Letcc to the interpreter.

Let's start with the standard infrastructure of values and environments.

sealed abstract class Value
type Env = Map[String, Value]
case class NumV(n: Int) extends Value
case class ClosureV(f: Fun, env: Env) extends Value

How do we represent values that represent continuations? Since we want to represent an object-language continuation by a meta-language continuation, we need to be able to wrap a meta-language continuation as an object-language value. This continuation will always accept some other object-language value:

case class ContV(f: Value => Nothing) extends Value

We also need a syntactic construct to apply continuations. One way to provide such a construct would be to add a new syntactic category of continuation application. We will instead do what Scheme and other languages also do: We overload the normal function-applicaton construct and also use it for application of continuations.

This means that we will need a case distinction in our interpreter whether the function argument is a closure or a continuation. Let's now study the interpreter for our new language. The branches for Num, Id, Add, and Fun are straightforward applications of the CPS transformation technique we already know.

def eval(e: Exp, env: Env, k: Value => Nothing): Nothing = e match {
  case Num(n: Int) => k(NumV(n))
  case Id(x) => k(env(x))
  case Add(l, r) => {
    eval(l, env, lv =>
        eval(r, env, rv =>
          (lv, rv) match {
            case (NumV(v1), NumV(v2)) => k(NumV(v1 + v2))
            case _ => sys.error("can only add numbers")
          }))
  }
  case f@Fun(param, body) => k(ClosureV(f, env))

  /* In the application case we now need to distinguish whether the first argument
   * is a closure or a continuation. If it is a continuation, we ignore the
   * current continuation k and "jump" to the stored continuation by applying the
   * evaluated continuation argument to it. */
  case Ap(f, a) => eval(f, env, cl => cl match {
            case ClosureV(f, closureEnv) => eval(a, env, av => eval(f.body, closureEnv + (f.param -> av), k))
            case ContV(f) => eval(a, env, av => f(av))
            case _ => sys.error("can only apply functions")
  })
  /* Letcc is now surprisingly simple: We continue the evaluation in the body in an
   * extended environment in which param is bound to the current continuation k,
   * wrapped as a value using ContV. */
  case Letcc(param, body) => eval(body, env + (param -> ContV(k)), k)
}

To make it easier to experiment with the interpreter this code provides the right initialization to eval. We have to give eval a continuation which represents the rest of the computation after eval is done. A small technical problem arises due to our usage of the return type Nothing for continuations, to emphasize that they do not return: The only way to implement a value that has this type is a function that does indeed not return. We do so by letting this function throw an exception. To keep track of the returned value we store it temporarily in a variable, catch the exception, and return the stored value.

def starteval(e: Exp): Value = {
  var res: Value = null
  val s: Value => Nothing = (v) => { res = v; sys.error("program terminated") }
  try { eval(e, Map.empty, s) } catch { case e: Throwable => () }
  res
}

Finally a small test of Letcc.

val testprog = Add(1, Letcc("k", Add(2, Ap("k", 3))))

assert(starteval(testprog) == NumV(4))

Here's a little quiz about Letcc:

Enable JavaScript to see the quiz

What is the result of the following expression?

    Add(Letcc("k", Add(3, Ap("k", 1))), 4)  
    
NumV(8) The addition of 3 is discarded. NumV(5) NumV(1) Don't forget the addition of 4. NumV(4) The captured continuation is the addition of 4.
Which of the following assertions about let/cc are correct? let/cc captures the whole remaining computation. If the current continuation is not used in the body, then let/cc behaves as if it was not there. The captured continuation is always used in the body of let/cc. It can also be discarded. let/cc is a control operator for delimited continuations. The captured continuation is undelimited.

LetCC

The content of this chapter is available as a Racket file here.

#lang racket

Racket is a language with so-called first-class continuations. It can reify the current continuation automatically and on the fly. As you may imagine, creating a continuation involves copying the stack, but there are less and more efficient ways of obtaining the same effect.

Adding continuations to a language makes it easy to create a better web programming protocol, as we have seen. But first-class continuations are much more general and give programmers immense power in numerous contexts.

In Racket (and the related programming language Scheme), a continuation is created with let/cc. It can be used to give the current continuation a name: (let/cc k ... k ...)

Let's write some programs using continuations (try this in the Racket read-eval-print loop).

(let/cc k (k 3)) What is the continuation k here?

(+ 1 (let/cc k (k 3))) What is the continuation k here?

Using let/cc for exception handling: let/cc acts as the "try", invoking k as the "throw".

(define (f n) (+ 10 (* 5 (let/cc k (/ 1 (if (zero? n) (k 1) n))))))

Next we simulate a very simple kind of exception handling mechanism with first-class continuations.

(define exceptionhandler (lambda (msg) (display "unhandled exception")))

(define (f n)
  (+ 5
     (if (zero? n) (exceptionhandler "division by zero")
         (/ 8 n))))

(define (g n)
  (+ (f n) (f n)))

(define (h)
  (let/cc k
    (begin
      (set! exceptionhandler (lambda (msg) (begin
                                             (displayln msg)
                                             (k))))
      (displayln (g 1))
      (displayln (g 0))
      (displayln (g 2)))))

Try evaluating (h) in the read-eval-print-loop.

Now we encode a simple debugger with support for breakpoints. The breakpoint variable stores the continuation at the current breakpoint

(define breakpoint false) ; initalized with a dummy value

The repl variable stores the continuation that jumps to the read-eval-print loop

(define repl false)       ; initialized with a dummy value

The break function captures the current continuation, stores it, and jumps to the REPL:

(define (break) (let/cc k
                  (begin
                    (set! breakpoint k)
                    (repl))))

To continue, we jump to the previously stored continuation:

(define (continue)
  (breakpoint))

Here is a simple test program of our "debugger":

(define (main)
  (display "1")
  (break)
  (display "2")
  (break)
  (display "3"))

; nothing to do after this, hence k is the REPL continuation
(let/cc k
  (set! repl k))

Let's now consider a more sophisticated usage of let/cc, namely to program a simple form of cooperative multi-threading, often called co-routines. A co-routine designates points in the routine where a switch to another routine should occur - a so-called yield point.

With let/cc we can program co-routines within the language, without having any dedicated built-in support for it:

(define queue empty)

(define (empty-queue?)
  (empty? queue))

(define (enqueue x)
  (set! queue (append queue (list x))))

(define (dequeue)
  (let ((x (first queue)))
    (set! queue (rest queue))
    x))

(define (fork)
  (let/cc k
    (begin
      (enqueue (lambda () (k 1))) ; enqueue thunk
      0)))

(define (join)
  (if (not (empty-queue?))
      ((dequeue))
      'alljoined))

(define (yield)
  (let/cc k
    (enqueue k)
    ((dequeue))))  ; invoke thunk

(define (fact n) (if (zero? n) 1 (* n (fact (- n 1)))))
(define (fib n) (if (< n 2) 1 (+ (fib (- n 1)) (fib (- n 2)))))

(define (printfibs n)
  (if (zero? n)
      (begin (print "Fertig mit fibs") (newline))
      (begin
        (print (format "Fib(~A)=~A" n (fib n)))
        (newline)
        (yield)
        (printfibs (- n 1)))))

(define (printfacts n)
  (if (zero? n)
      (begin (print "Fertig mit facts") (newline))
      (begin
        (print (format "Fact(~A)=~A" n (fact n)))
        (newline)
        (yield)
        (printfacts (- n 1)))))


(define (test-forkjoin)
  (if (= (fork) 0)
      (printfibs 8)
      (printfacts 12))
  (join)
  (if (= (fork) 0)
    (printfibs 10)
    (printfacts 8))
  (join))

Delimited Continuations

The content of this chapter is available as a Scala file here.

The continuations we have seen so far represent the whole call-stack. Invoking a continuation was not like a function call because continuations never return a result. Invoking a continuation is more similar to a disciplined version of GOTO.

However, the fact that continuations never return and represent the full call-stack often makes their use cumbersome. In particular, such continuations cannot be composed. The non-composability of continuations is visible in the fact that applications using first-class continuations often need to make use of mutable state.

This is different with delimited continuations. Delimited continuations only represent a segement of the call-stack. Delimited continuations behave like functions, that is, they return a value and are hence composable.

Delimited continuations have many quite powerful applications, ranging from advanced exception handling to backtracking algorithms and probabilistic programming as well as so-called algebraic effects. Delimited continuations are available in Racket and many variants of Scheme, OCaML, Haskell and, thanks to work in our research group, in Java.

There are many different variants of delimited continuations, many of them dating back to the late 1980s and early 1990s. One of the most common forms of delimited continuations is in the form of reset and shift, proposed by Olivier Danvy and Andrzej Filinski in 1990. The first of these primitives, reset e, marks the current stack frame and continues with e. An invocation of the second primitive, shift k e, reifies the stack segment between the current stack frame and the closest stack frame marked by a reset as a function, binds this function to k, and evaluates e (which can then call k zero or more times).

Their meaning can be understood via a code transformation.

reset (...A... shift k e ...B...)
 ; -->
with k = lambda x. reset (...A... x ...B...) :
  reset e

reset e  ; no invocation of shift inside e
 ; -->
e

In class, we will look at a couple of examples of using shift and reset. shift and reset are available in some variants of Scheme, including Racket, as an extension of Scala, as a library in OcaML, as well as various usually partial, incomplete or buggy simulations in other languages.

A definitional interpreter for delimited continuations is pretty simple.

enum Exp:
  case Num(n: Int)
  case Id(name: String)
  case Add(lhs: Exp, rhs: Exp)
  case Fun(param: String, body: Exp)
  case Ap(funExpr: Exp, argExpr: Exp)
  case Shift(param: String, body: Exp)
  case Reset(body: Exp)

object Exp:
  implicit def num2exp(n: Int): Exp = Num(n)
  implicit def id2exp(s: String): Exp = Id(s)
sealed abstract class Value
type Env = Map[String, Value]
case class NumV(n: Int) extends Value
case class ClosureV(f: Fun, env: Env) extends Value
case class ContV(f: Value => Value) extends Value

def eval(e: Exp, env: Env, k: Value => Value): Value = e match {
  case Num(n: Int) => k(NumV(n))
  case Id(x) => k(env(x))
  case Add(l, r) => {
    eval(l, env, lv =>
        eval(r, env, rv =>
          (lv, rv) match {
            case (NumV(v1), NumV(v2)) => k(NumV(v1 + v2))
            case _ => sys.error("can only add numbers")
          }))
  }
  case f@Fun(param, body) => k(ClosureV(f, env))

  case Ap(f, a) => eval(f, env, cl => cl match {
            case ClosureV(f, closureEnv) => eval(a, env, av => eval(f.body, closureEnv + (f.param -> av), k))
            // compose continuations k2 and k
            case ContV(k2) => eval(a, env, av => k(k2(av)))
            case _ => sys.error("can only apply functions")
  })
  // reset the continuation to the identity function
  case Reset(e) => k(eval(e, env, x => x))
  // wrap current continuation and reset continuation
  case Shift(param, body) => eval(body, env + (param -> ContV(k)), x => x)
}

Enable JavaScript to see the quiz

What is the part of the computation captured by Shift in the following expression?

  Add(3,
    Reset(Add(1,
      Shift("k", Add(4, Ap("k", 2))))))
    
Add(1, -) Add(3, -) The part inside of Reset is captured. Add(3, Add(1, -) Only the part inside of Reset is captured. Add(4, -) The part outside of Shift is captured.
What is the result of evaluating the following expression?

  Add(
    Reset(Add(2,
      Shift("k", Add(Ap("k", 1), Ap("k", 3))))),  
    5)
    
Num(8) The part outside of Reset is not discarded. Num(13) Num(18) The part outside of Reset is not captured. sys.error("can only add numbers") Applying the captured continuation to a number results in a number.

References:

  1. Olivier Danvy and Andre Filinski, “Abstracting control”, LISP and Functional Programming, 1990.
  2. O. Kiselyov, An argument against call/cc.
  3. O. Kiselyov, Introduction to programming with shift and reset.

Introduction to Monads

The content of this chapter is available as a Scala file here.

We have seen various patterns of function composition:

  • The environment-passing style, in which an environment is passed down in recursive calls.
  • The store-passing style, in which a store is threaded in and out of every computation.
  • The continuation-passing style, in which every function call is a tail call.

Monads are way to abstract over such patterns of function composition.

Motivation

Using monads, we can write code which can be parameterized to be in one of the styles above (or many others). Here is another common pattern of function composition. Suppose we have the following API of (nonsensical) functions:

def f(n: Int): String = "x"
def g(x: String): Boolean = x == "x"
def h(b: Boolean): Int = if (b) 27 else sys.error("error")

def clientCode = h(!g(f(27) + "z"))

Now suppose that these functions can possibly fail (say, because they involve remote communication). A common way to deal with such failures is to use the Option datatype:

def fOp(n: Int): Option[String] = if (n < 100) Some("x") else None
def gOp(x: String): Option[Boolean] = Some(x == "x")
def hOp(b: Boolean): Option[Int] = if (b) Some(27) else None

However, now the clientCode must be changed rather dramatically:

def clientCodeOp =
  fOp(27) match {
    case Some(x) => gOp(x + "z") match {
        case Some(y) => hOp(!y)
        case None => None
      }
    case None => None
  }

We see a kind of pattern in this code. We have a value of type Option[A], but the next function we need to call requires an A and produces an Option[B]. If the Option[A] value is None, then the whole computation produces None. If it is Some(x) instead, we pass x to the function.

We can capture this pattern in the form of a function:

def bindOption[A, B](a: Option[A], f: A => Option[B]): Option[B] = a match {
  case Some(x) => f(x)
  case None => None
}

Using bindOption, we can rewrite the code above as follows:

def clientCodeOpBind =
  bindOption(fOp(27), (x: String) =>
    bindOption(gOp(x + "z"), (y: Boolean) =>
      hOp(!y)))

Now suppose that our original client code was not h(!g(f(27) + "z")) but instead !g(f(27) + "z"). How can we express this with bindOption? This thing does not type check:

def clientCode =
  bindOption(f(27), (x: String) =>
    bindOption(g(x + "z"), (y: Boolean) =>
      !y))

One way to fix the situation is to insert a call to Some, like so:

def clientCode2Op =
  bindOption(fOp(27), (x: String) =>
    bindOption(gOp(x + "z"), (y: Boolean) =>
      Some(!y)))

While this works, it is incompatible with our original goal of abstracting over the function composition pattern, because the Some constructor exposes what kind of pattern we are currently dealing with. Hence let's abstract over it by adding a second function unit to our function composition interface:

def unit[A](x: A): Option[A] = Some(x)

def clientCode2OpUnit =
  bindOption(fOp(27), (x: String) =>
    bindOption(gOp(x + "z"), (y: Boolean) =>
      unit(!y)))

This looks better, but the types of unit and bindOption (and also the name, but we can of course change that) still reveal that we are dealing with the Option function-composition pattern. Let's abstract over the Option type constructor by turning the type constructor into a parameter. The resulting triple (type constructor, unit function, bind function) is called a monad. Certain conditions (the "monad laws") on unit and bind also need to hold to make it a true monad, but we'll defer a discussion of these conditions until later.

The Monad Interface

So here it is: The Monad interface.

trait Monad[M[_]] {
  def unit[A](a: A): M[A]
  def bind[A, B](m: M[A], f: A => M[B]): M[B]
  // The "monad laws":
  // 1) "unit" acts as a kind of neutral element of "bind", that is:
  //    1a) bind(unit(x), f) == f(x) and
  //    1b) bind(x, y => unit(y)) == x
  // 2) Bind enjoys an associative property
  //     bind(bind(x, f), g) == bind(x, y => bind(f(y), g))
}

Using this interface, we can now make clientCode depend only on this interface, but no longer on the Option type:

def clientCode2Op(m: Monad[Option]) =
  m.bind(fOp(27), (x: String) =>
    m.bind(gOp(x + "z"), (y: Boolean) =>
      m.unit(!y)))

If the API is parametric in the monad, we can make the client code fully parametric, too. We model the monad object as an implicit parameter to save the work of passing on the monad in every call.

def fM[M[_]](n: Int)(using m: Monad[M]): M[String] = sys.error("not implemented")
def gM[M[_]](x: String)(using m: Monad[M]): M[Boolean] = sys.error("not implemented")
def hM[M[_]](b: Boolean)(using m: Monad[M]): M[Int] = sys.error("not implemented")

def clientCode2[M[_]](using m: Monad[M]) =
  m.bind(fM(27), (x: String) =>
    m.bind(gM(x + "z"), (y: Boolean) =>
      m.unit(!y)))

For-Comprehension Syntax

All these nested calls to bind can make the code hard to read. Luckily, there is a notation called "monad-comprehension" to make monadic code look simpler. Monad-comprehensions are directly supported in Haskell and some other languages. In Scala, we can piggy-back on the "for-comprehension" syntax instead. A "for-comprehension" is usually used for lists and other collections. For instance:

val l = List(List(1, 2), List(3, 4))
assert((for { x <- l; y <- x } yield y + 1) == List(2, 3, 4, 5))

The Scala compiler desugars the for-comprehension above into calls of the standard map and flatMap functions. That is, the above for-comprehension is equivalent to:

assert(l.flatMap(x => x.map(y => y + 1)) == List(2, 3, 4, 5))

We will make use of for-comprehension syntax by supporting both flatMap (which is like bind) and map (which is like fmap). We support these functions by an implicit conversion to an object that supports these functions as follows:

extension [A, M[_]](m: M[A])(using mm: Monad[M])
  def map[B](f: A => B): M[B] = mm.bind(m, (x: A) => mm.unit(f(x)))
  def flatMap[B](f: A => M[B]): M[B] = mm.bind(m, f)

Using the new support for for-comprehension syntax, we can rewrite our client code as follows: Given the API from above,

def fOp(n: Int): Option[String] = if (n < 100) Some("x") else None
def gOp(x: String): Option[Boolean] = Some(x == "x")
def hOp(b: Boolean): Option[Int] = if (b) Some(27) else None

We can now rewrite this

def clientCode2Op(m: Monad[Option]) =
  m.bind(fOp(27), (x: String) =>
    m.bind(gOp(x + "z"), (y: Boolean) =>
      m.unit(!y)))

to this:

def clientCode2OpFor(using m: Monad[Option]) =
  for {
    x <- fOp(27)
    y <- gOp(x + "z")
  } yield !y

The Option Monad

Let's look at some concrete monads now. We have of course already seen one particular monad: The Option monad. This monad is also sometimes called the Maybe monad.

object OptionMonad extends Monad[Option] {
  override def bind[A, B](a: Option[A], f: A => Option[B]): Option[B] =
    a match {
      case Some(x) => f(x)
      case None => None
    }
  override def unit[A](a: A) = Some(a)
}

We can now parameterize clientCode with OptionMonad.

def v: Option[Boolean] = clientCode2Op(OptionMonad)

Generic Functions for Monads

There are many other sensible monads. Before we discuss those, let us discuss whether there are useful functions that are generic enough to be useful for many different monads. Here are some of these functions:

fmap turns every function between A and B into a function between M[A] and M[B]:

def fmap[M[_], A, B](f: A => B)(using m: Monad[M]): M[A] => M[B] =
  a => m.bind(a, (x: A) => m.unit(f(x)))

In fancy category theory terms, we can say that every monad is a functor.

sequence composes a list of monadic values into a single monadic value which is a list.

def sequence[M[_], A](l: List[M[A]])(using m: Monad[M]): M[List[A]] = l match {
  case x :: xs => m.bind(x, (y: A) =>
    m.bind(sequence(xs), (ys: List[A]) =>
      m.unit(y :: ys)))
  case Nil => m.unit(List.empty)
}

mapM composes sequence and the standard map function:

def mapM[M[_], A, B](f: A => M[B], l: List[A])(using m: Monad[M]): M[List[B]] =
  sequence(l.map(f))

join is another useful function to unwrap a layer of monads. In category theory, monads are defined via unit (denoted by the greek letter η) and join (denoted μ) instead of unit and bind. There are additional "naturality" and "coherence conditions" that make the category theory definition equivalent to ours.

def join[M[_], A](x: M[M[A]])(using m: Monad[M]): M[A] = m.bind(x, (y: M[A]) => y)

Here are some other common monads:

The Identity Monad

The identity monad is the simplest monad which corresponds to ordinary function application. If we parameterize monadic code with the identity monad, we get the behavior of the original non-monadic code.

type Id[X] = X
object IdentityMonad extends Monad[Id] {
  def bind[A, B](x: A, f: A => B): B = f(x)
  def unit[A](a: A): A = a
}

The Reader Monad

This is the reader monad, a.k.a. environment monad. It captures the essence of "environment-passing style".

The type parameter [A] =>> R => A may look a bit complicated, but it is merely "currying" the function arrow type constructor.

The type constructor which is created here is M[A] = R => A

trait ReaderMonad[R] extends Monad[[A] =>> R => A] {
  // pass the "environment" r into both computations
  override def bind[A, B](x: R => A, f: A => R => B): R => B = r => f(x(r))(r)
  override def unit[A](a: A): R => A = (_) => a
}

Example: Suppose that all functions in our API above depend on some kind of environment, say, the current configuration. For simplicitly, let's assume that the current configuration is just an Int, hence all functions have a return type of the form Int => A:

def fRead(n: Int): Int => String = sys.error("not implemented")
def gRead(x: String): Int => Boolean = sys.error("not implemented")
def hRead(b: Boolean): Int => Int = sys.error("not implemented")

Our original code,

def clientCode = h(!g(f(27) + "z"))

becomes:

def clientCodeRead(env: Int) = hRead(!gRead(fRead(27)(env) + "z")(env))(env)

In monadic form, the explicit handling of the environment disappears again:

def clientCode2Read(using m: ReaderMonad[Int]) =
  m.bind(fRead(27), (x: String) =>
    m.bind(gRead(x + "z"), (y: Boolean) =>
      m.unit(!y)))

/** this code does not work in older versions of Scala */
def clientCode2ReadFor(using m: ReaderMonad[Int]) =
  for {
    x <- fRead(27)
    y <- gRead(x + "z")
  } yield !y

The State Monad

The state monad, in which computations depend on a state S which is threaded through the computations, is defind as follows:

trait StateMonad[S] extends Monad[[A] =>> S => (A, S)] {
  override def bind[A, B](x: S => (A, S), f: A => S => (B, S)): S => (B, S) =
    // thread the state through the computations
    s => x(s) match { case (y, s2) => f(y)(s2) }
  override def unit[A](a: A): S => (A, S) = s => (a, s)
}

Example: Assume that our API maintains a state which (for simplicity) we assume to be a single integer. That is, it would look like this:

def fState(n: Int): Int => (String, Int) = sys.error("not implemented")
def gState(x: String): Int => (Boolean, Int) = sys.error("not implemented")
def hState(b: Boolean): Int => (Int, Int) = sys.error("not implemented")

The original code,

def clientCode = h(!g(f(27) + "z"))

becomes:

def clientCodeState(s: Int) =
  fState(27)(s) match {
    case (x, s2) => gState(x + "z")(s2) match {
      case (y, s3) => hState(!y)(s3) }}

In monadic style, however, the state handling disappears once more:

def clientCode2State(using m: StateMonad[Int]) =
  m.bind(fState(27), (x: String) =>
    m.bind(gState(x + "z"), (y: Boolean) =>
      m.unit(!y)))

The List Monad

In the list monad, computations produce lists of results. The bind operator combines all those results in a single list.

object ListMonad extends Monad[List] {
  // apply f to each element, concatenate the resulting lists
  override def bind[A, B](x: List[A], f: A => List[B]): List[B] = x.flatMap(f)
  override def unit[A](a: A) = List(a)
}

Example: Assume that our API functions return lists of results, and our client code must exercise the combination of all possible answers.

def fList(n: Int): List[String] = sys.error("not implemented")
def gList(x: String): List[Boolean] = sys.error("not implemented")
def hList(b: Boolean): List[Int] = sys.error("not implemented")

The original code,

def clientCode = h(!g(f(27) + "z"))

becomes:

def clientCodeList =
  fList(27).map(x => gList(x + "z")).flatten.map(y => hList(!y)).flatten

The monadic version of the client code stays the same, as expected:

def clientCode2List = {
  given Monad[List] = ListMonad
  for {
    x <- fList(27)
    y <- gList(x + "z")
  } yield !y
}

The Continuation Monad

The last monad we are going to present is the continuation monad, which stands for computations that are continuations.

trait ContinuationMonad[R] extends Monad[[A] =>> (A => R) => R] {
  type Cont[X] = (X => R) => R

  override def bind[A, B](x: Cont[A], f: A => Cont[B]): Cont[B] =
    // construct continuation for x that calls f with the result of x
    k => x(a => f(a)(k))
  override def unit[A](a: A): Cont[A] = k => k(a)

  // callcc is like letcc; the difference is that letcc binds a name,
  // whereas callcc expects a function as argument.
  // That means that letcc(k, ...) is expressed as callcc(k => ...).
  def callcc[A, B](f: (A => Cont[B]) => Cont[A]): Cont[A] =
    k => f((a: A) => (_: B => R) => k(a))(k)
}

Example: Suppose our API was CPS-transformed:

def fCPS[R](n: Int): (String => R) => R = sys.error("not implemented")
def gCPS[R](x: String): (Boolean => R) => R = sys.error("not implemented")
def hCPS[R](b: Boolean): (Int => R) => R = sys.error("not implemented")

The original code,

def clientCode = h(!g(f(27) + "z"))

becomes:

def clientCodeCPS[R]: (Int => R) => R =
  k => fCPS(27)((x: String) => gCPS(x + "z")((y: Boolean) =>  hCPS(!y)(k)))

The monadic version hides the CPS transformation in the operations of the monad.

def clientCode2CPS[R](using m: ContinuationMonad[R]) =
  m.bind(fCPS(27), (x: String) =>
    m.bind(gCPS(x + "z"), (y: Boolean) =>
      m.unit(!y)))

Let's implement 1 + (2 + 3) in monadic style and implicitly CPS-transform using the continuation monad:

// unfortunately we can, again, not use for-comprehension syntax
def ex123[R](using m: ContinuationMonad[R])  = {
  m.bind(
    m.bind(m.unit(2), (two: Int) =>
      m.bind(m.unit(3), (three: Int) => m.unit(two + three))),
    (five: Int) => m.unit(1 + five))
}

def runEx123 = ex123(using new ContinuationMonad[Int]{})(x => x)

Let's implement the (+ 1 (let/cc k (+ 2 (k 3)))) example using callcc

def excallcc[R](using m: ContinuationMonad[R])  = {
  m.bind(
    m.bind(m.unit(2), (two: Int) =>
      m.callcc[Int, Int](k => m.bind(k(3), (three: Int) => m.unit(two + three)))),
    (five: Int) => m.unit(1 + five))
}

def runExcallcc = excallcc(using new ContinuationMonad[Int]{})(x => x)

Remember how we had to CPS-transform the map function in the "allCosts" example when we talked about continuations? Now we can define a monadic version of map that works for any monad, including the continuation monad:

def mapM[M[_], A, B](x: List[A], f: A => M[B])(using m: Monad[M]): M[List[B]] =
  sequence(x.map(f))

Monad Transformers

The purpose of monad transformers is to compose monads. For instance, what if we want to have both the list monad and the option monad at the same time? Such situations arise very often in practical code.

One solution is to have a monad transformer version of each monad, which is parameterized with another monad. We show this for the Option monad case.

type OptionT[M[_]] = [A] =>> M[Option[A]]

class OptionTMonad[M[_]](val m: Monad[M]) extends Monad[OptionT[M]] {

  override def bind[A, B](x: M[Option[A]], f: A => M[Option[B]]): M[Option[B]] =
    m.bind(x, (z: Option[A]) => z match { case Some(y) => f(y)
                                          case None => m.unit(None) })

  override def unit[A](a: A) = m.unit(Some(a))

  def lift[A](x: M[A]): M[Option[A]] = m.bind(x, (a: A) => m.unit(Some(a)))
}
val ListOptionM = new OptionTMonad(ListMonad)
// in this case, for-comprehension syntax doesn't work because it clashes with the
// built-in support for for-comprehensions for lists :-(
def example = {
  val m = ListOptionM
  m.bind(List(Some(3), Some(4)), (x: Int) => m.unit(x + 1))
}

def example2 = {
  val m = ListOptionM
  m.bind(List(Some(3), Some(4)), (x: Int) => m.lift(List(1, 2, 3, x)))
}

def example3 = {
  val m = ListOptionM
  m.bind(List(Some(3), None, Some(4)), (x: Int) => m.unit(x + 1))
}

def example4 = {
  val m = ListOptionM
  m.bind(List(Some(3), Some(4)), (x: Int) => if (x > 3) m.m.unit(None) else m.unit(x * 2))
}

Monad transformers are a standard way to compose monads, e.g., in Haskell and in Scala. They have a number of well-known disadvantages. For instance, one needs additional transformer versions of monads and the required lifting sometimes destroys modularity. There are a number of alternative proposals to monad transformers, such as "extensible effects".

IO Monad

The content of this chapter is available as a Scala file here.

trait IOMonad {
  type IO[_]
  def unit[A](a: A): IO[A]
  def bind[A, B](m: IO[A], f: A => IO[B]): IO[B]
  def printString(s: String): IO[Unit]
  def inputString: IO[String]

  def performIO[A](action: IO[A]): A
}
val iomonad: IOMonad = new IOMonad {
  type World = String
  type IO[A] = World => (A, World)
  def unit[A](a: A): IO[A] = w => (a, w)
  def bind[A, B](m: IO[A], f: A => IO[B]): IO[B] =
    w => m(w) match { case (a, w2) => f(a)(w2) }
  def printString(s: String): IO[Unit] =
    w => { println(s); ((), w + s + " was printed and then ...\n") }
  def inputString: IO[String] =
    w => {
      val input = scala.io.StdIn.readLine();
      (input, w + input + " was entered and then ...\n")
    }

  def performIO[A](action: IO[A]): A =
    action("The world in which nothing has happened yet, but then ...\n") match {
      case (a, w) =>
        println("Peformed all actions. The world in which all this happened is: \n" + w); a
    }
}
def someIOActions(implicit m: IOMonad): m.IO[Unit] =
  m.bind(m.printString("Enter your first name:"), (_: Unit) =>
  m.bind(m.inputString, (firstName: String) =>
  m.bind(m.printString("Enter your last name:"), (_: Unit) =>
  m.bind(m.inputString, (lastName: String) =>
  m.printString("Hello, " + firstName + " " + lastName + "!")))))

def test = iomonad.performIO(someIOActions(iomonad))

Modular Interpreters

The content of this chapter is available as a Scala file here.

A Monad Library

We define a Monad library in Scala. Monad libraries like in scalaz and cats (for Scala) or the Haskell standard library look similar.

trait Monad {
  type M[_] // this time we treat M as a type member and not as type parameter
            // because it leads to shorter type signatures
  def unit[A](a: A): M[A]
  def bind[A, B](m: M[A], f: A => M[B]): M[B]

  extension [A](m: M[A])
    def map[B](f: A => B): M[B] = bind(m, (x: A) => unit(f(x)))
    def flatMap[B](f: A => M[B]): M[B] = bind(m, f)
}

We formulate concrete monads in the form of abstract interfaces first. The idea of those interfaces is that it should be possible to use the monad only in terms of that interface, without knowing anything about M. The advantage of this approach is that it enables us to compose monads. M changes in every composition of monads. For instance, when composing the list monad and the option monad, then M[X] = Option[List[X]] or M[X] = List[Option[X]].

By keeping M abstract and using it only via the interfaces, "client code" does not need to depend on the particular composition of monads.

Reader Monad

The Reader (or Environment) monad captures computations that depend on an environment of type R.

The ask function yields the current environment, the local function is used to transform the environment in a subcomputation a by an environment transformer f.

trait ReaderMonad extends Monad {
  type R
  def ask: M[R]
  def local[A](f: R => R, a: M[A]): M[A]
}

The standard implementation of the Reader monad:

trait ReaderMonadImp extends ReaderMonad {
  type M[X] = R => X
  def unit[A](a: A): M[A] = r => a
  def bind[A, B](m: M[A], f: A => M[B]): M[B] = r => f(m(r))(r)
  def ask: M[R] = identity
  def local[A](f: R => R, a: M[A]): M[A] = r => a(f(r))
}

An example of using the Reader monad to propagate an environment of type Int through a computation.

object ReaderExample extends ReaderMonadImp {
  type R = Int
  def example: M[Int] = for { x <- ask } yield (x + 1)
  def example2: M[Int] = local(r => 99, example)
}

A more useful example where we use the Reader monad to propagate a mapping from identifiers to boolean values in an interpreter for boolean formulas. Note that the signature of eval is identical to def eval(e: Exp): Map[String, Boolean] => Boolean, that is, we curry eval to make it applicable to the Reader monad.

object ReaderExample2 extends ReaderMonadImp {
  enum Exp:
    case Id(x: String)
    case And(l: Exp, r: Exp)
    case Or(l: Exp, r: Exp)
  import Exp._
  type R = Map[String, Boolean]

  def eval(e: Exp): M[Boolean] = e match {
    case Id(x) => for {env <- ask } yield env(x)
    case And(l, r) => for {
      x <- eval(l)
      y <- eval(r)
    } yield (x && y)
    case Or(l, r) => for {
      x <- eval(l)
      y <- eval(r)
    } yield (x || y)
  }
}

The implementation of the And case is semantically equivalent to this code:

case And(l, r) => env => {
  val x = eval(l)(env)
  val y = eval(r)(env)
  x && y
}

However, the monadic code is more abstract (and hence more reusable) because it is not coupled to the concrete M.

State Monad

This is the interface and standard implementation for the State monad:

trait StateMonad extends Monad {
  type S
  def getState: M[S]
  def putState(s: S): M[Unit]
}

trait StateMonadImp extends StateMonad {
  type M[A] = S => (A, S)
  def unit[A](a: A): M[A] = (s: S) => (a, s)
  def bind[A, B](m: M[A], f: A => M[B]): M[B] = (s: S) => {
    val (a, s2) = m(s)
    f(a)(s2)
  }
  def getState: M[S] = s => (s, s)
  def putState(s: S): M[Unit] = _ => ((), s)
}

Continuation Monad

And here is the interface for the Continuation monad. The Continuation monad provides a method callcc, which reifies the current Continuation k: A => M[B].

trait ContinuationMonad extends Monad {
  def callcc[A, B](f: (A => M[B]) => M[A]): M[A]
}

Implementaions

Now we provide implementations of the monad interfaces.

The identity monad, which is the end of each transformer chain reads:

trait IdentityMonad extends Monad {
  type M[A] = A
  def unit[A](a: A): M[A] = a
  def bind[A, B](m: M[A], f: A => M[B]) = f(m)
}

object IdentityMonad extends IdentityMonad

We organize most other monads as monad transformers. A monad transformer is parameterized with another monad. The monads are organized in a chain. Operations of "inner" monads must be lifted to top-level operations.

trait MonadTransformer extends Monad {
  val m: Monad
}

Reader Monad Transformer

The Reader monad transformer. We provide some convenient functions lift, lift2 etc. to lift functions from the inner monad. Note that M[X] = R => m.M[X] instead of M[X] = R => X (as for the non-transformer version of the Reader monad). The correct implementation of the interface methods follows from this type equation.

trait ReaderT extends MonadTransformer with ReaderMonad {
  type R
  override type M[X] = R => m.M[X]
  override def unit[A](a: A): M[A] = r => m.unit(a)
  override def bind[A, B](x: M[A], f: A => M[B]): M[B] =
    r => m.bind(x(r), (n: A) => f(n)(r))
  override def ask: M[R] = r => m.unit(r)
  override def local[A](f: R => R, a: M[A]): M[A] = r => a(f(r))
  protected implicit def lift[A](x: m.M[A]): M[A] = r => x
  protected implicit def lift2[A, B](x: A => m.M[B]): A => M[B] = a => lift(x(a))
  protected implicit def lift3[A, B, C](x: (A => m.M[B]) => m.M[C]): (A => M[B]) => M[C] =
    f => r => x((a: A) => f(a)(r))
  protected implicit def lift4[A, B, C, D](x: ((A => m.M[B]) => m.M[C]) => m.M[D]): ((A => M[B]) => M[C]) => M[D] =
    f => r => x((a: A => m.M[B]) => f(lift2(a))(r))
}

// The original Reader monad can be reconstructed by composing ReaderT with the identity monad.

trait ReaderMonadImpl extends ReaderT {
  val m: IdentityMonad = IdentityMonad
}

We do not need this because we have just synthesized it:

trait ReaderMonadImpl extends ReaderMonad {
  type M[X] = R => X
  def unit[A](a: A): M[A] = r => a
  def bind[A, B](m: M[A], f: A => M[B]): M[B] = r => f(m(r))(r)
  def ask: M[R] = identity
  def local[A](f: R => R, a: M[A]): M[A] = (r) => a(f(r))
}

State Monad Transformer

The design of StateT is similar to that of ReaderT:

trait StateT extends MonadTransformer with StateMonad {
  type M[A] = S => m.M[(A, S)]
  override def unit[A](a: A): M[A] = (s: S) => m.unit(a, s)
  override def bind[A, B](x: M[A], f: A => M[B]): M[B] = (s: S) => {
     m.bind[(A, S), (B, S)](x(s), { case (a, s2) => f(a)(s2)})
  }
  override def getState: M[S] = s => m.unit((s, s))
  override def putState(s: S): M[Unit] = _ => m.unit(((), s))
}

// and again we can reconstruct the ordinary State monad.

trait StateMonadImpl extends StateT {
  val m: IdentityMonad = IdentityMonad
}

We do not need this because we have just synthesized it:

trait StateMonadImpl extends StateMonad {
  type M[A] = S => (A, S)
  def unit[A](a: A): M[A] = (s: S) => (a, s)
  def bind[A, B](m: M[A], f: A => M[B]): M[B] = (s: S) => {
     val (a, s2) = m(s)
     f(a)(s2)
  }
  def getState: M[S] = s => (s, s)
  def putState(s: S): M[Unit] = _ => ((), s)
}

Continuation Monad

We could also synthesize ContinuationMonadImpl from a ContT just as we did for ReaderMonadImpl and StateMonadImpl, but for simplicity we only present the ordinary Continuation monad here.

trait ContinuationMonadImpl extends ContinuationMonad {
  type T
  type M[A] = (A => T) => T
  override def unit[A](a: A): M[A] = k => k(a)
  override def bind[A, B](m: M[A], f: A => M[B]): M[B] = k => m(a => f(a)(k))
  override def callcc[A, B](f: (A => M[B]) => M[A]): M[A] = k => f(a => _ => k(a))(k)
}

Compositions

Let's compose some monads.

The composition of the Reader monad and some Continuation monad.

trait ReaderContinuationMonadForwarder extends ReaderT with ContinuationMonad {
  val m: ContinuationMonad
  // call to lift4 inserted automatically
  override def callcc[A, B](f: (A => M[B]) => M[A]): M[A] = (m.callcc[A, B] _)(f)
}

Fro the implementation, we use the Continuation-monad implementation.

trait ReaderContinuationMonadImpl extends ReaderContinuationMonadForwarder {
  type T
  val m: ContinuationMonadImpl { type T = ReaderContinuationMonadImpl.this.T } =
    new ContinuationMonadImpl { type T = ReaderContinuationMonadImpl.this.T }
}

Composition of the Reader monad with some State monad.

trait ReaderStateMonadForwarder extends ReaderT with StateMonad {
  val m: StateMonad { type S = ReaderStateMonadForwarder.this.S }
  override def getState: M[S] = m.getState
  override def putState(s: S): M[Unit] = m.putState(s)
}

And the implementation with StateMonadImpl.

trait ReaderStateMonadImpl extends ReaderStateMonadForwarder {
  val m: StateMonadImpl { type S = ReaderStateMonadImpl.this.S } =
    new StateMonadImpl { type S = ReaderStateMonadImpl.this.S }
}

A Modular Interpreter

Now we use the monad library to modularize the interpreters of the various language variants we have seen so far.

trait Expressions extends Monad {
  abstract class Value
  abstract class Exp {
    def eval: M[Value]
  }
}

trait Numbers extends Expressions {
  case class NumV(n: Int) extends Value
}

trait Arithmetic extends Numbers {
  case class Num(n: Int) extends Exp {
    def eval = unit(NumV(n))
  }
  implicit def num2exp(n: Int): Exp = Num(n)

  case class Add(lhs: Exp, rhs: Exp) extends Exp {
    def eval = for {
                 l <- lhs.eval
                 r <- rhs.eval
               } yield (l, r) match {
                 case (NumV(v1), NumV(v2)) => NumV(v1 + v2)
                 case _ => sys.error("can only add numbers")
               }
  }
}

trait If0 extends Numbers {
  case class If0(cond: Exp, thenExp: Exp, elseExp: Exp) extends Exp {
    def eval = for {
                 c <- cond.eval
                 res <- c match {
                   case NumV(0) => thenExp.eval
                   case _ => elseExp.eval
                 }
               } yield res
  }
}

trait Functions extends Expressions with ReaderMonad {
  type Env = Map[String, Value]
  override type R = Env

  case class ClosureV(f: Fun, env: Env) extends Value
  case class Fun(param: String, body: Exp) extends Exp {
    def eval = for { env <- ask } yield ClosureV(this, env)
  }
  case class Ap(f: Exp, a: Exp) extends Exp {
    def eval = for {
                 fv <- f.eval
                 av <- a.eval
                 res <- fv match {
                   case ClosureV(fun, cenv) =>
                     local(env => cenv + (fun.param -> av), fun.body.eval)
                 }
               } yield res
  }
  case class Id(x: String) extends Exp {
    def eval = for {
                 env <- ask
               } yield env(x)
  }
  implicit def id2exp(x: String): Exp = Id(x)
  def wth(x: String, xdef: Exp, body: Exp): Exp = Ap(Fun(x, body), xdef)
}


trait Boxes extends Expressions with StateMonad  {
  type Store = Map[Address, Value]
  override type S = Store

  type Address = Int
  var _nextAddress = 0

  def nextAddress: Address = {
    _nextAddress += 1
    _nextAddress
  }

  case class AddressV(a: Address) extends Value

  case class NewBox(e: Exp) extends Exp {
    def eval = {
      val a = nextAddress
      for {
        v <- e.eval
        s <- getState
        _ <- putState(s + (a -> v))
      } yield AddressV(a)
    }
  }
  case class SetBox(b: Exp, e: Exp) extends Exp {
    def eval =
      for {
         box <- b.eval
         ev  <- e.eval
         s   <- getState
         _   <- putState(box match { case AddressV(a) => s.updated(a, ev) })
      } yield ev
  }
  case class OpenBox(b: Exp) extends Exp {
    def eval = for {
                 bv <- b.eval
                 s  <- getState
               } yield (bv match { case AddressV(a) => s(a) })
  }
  case class Seq(e1: Exp, e2: Exp) extends Exp {
    def eval = bind(e1.eval, (_: Value) => e2.eval)
  }

}

trait Letcc extends Expressions with ContinuationMonad with ReaderMonad {
  override type R = Map[String, Value]

  // We introduce a new application form CAp instead of using Ap because we cannot extend Ap
  case class CAp(f: Exp, a: Exp) extends Exp {
    override def eval: M[Value] =
      for {
         fv <- f.eval
         av <- a.eval
         res <- fv match { case ContV(f) => f(av) }
       } yield res
  }
  case class Letcc(param: String, body: Exp) extends Exp {
    override def eval: M[Value] =
      callcc[Value, Value](k => local(env => env + (param -> ContV(k)), body.eval))
  }
  case class ContV(f: Value => M[Value]) extends Value
}

Let's compose together some languages!

object AE extends Arithmetic with IdentityMonad {
  val aetest = Add(1, Add(2, 3))
}
assert(AE.aetest.eval == AE.NumV(6))

object FAELang extends Functions with Arithmetic with ReaderMonadImpl {
  val faetest = Ap(Fun("x", Add("x", 1)), Add(2, 3))
  assert(faetest.eval(Map.empty) == NumV(6))
}
object BCFAE extends Boxes with Arithmetic with Functions with If0 with ReaderStateMonadImpl {
  val test = wth("switch", NewBox(0),
                wth("toggle", Fun("dummy", If0(OpenBox("switch"),
                                          Seq(SetBox("switch", 1), 1),
                                          Seq(SetBox("switch", 0), 0))),
                    Add(Ap("toggle", 42), Ap("toggle", 42))))
}

assert(BCFAE.test.eval(Map.empty)(Map.empty)._1 == BCFAE.NumV(1))

object FAEwLetcc extends Arithmetic with Functions with If0 with Letcc with ReaderContinuationMonadImpl {
  override type T = Value
  val testprog = Add(1, Letcc("k", Add(2, CAp("k", 3))))
}

assert(FAEwLetcc.testprog.eval(Map.empty)(identity) == FAEwLetcc.NumV(4))

Monadic Reflection

The content of this chapter is available as a Racket file here.

#lang racket

(require racket/control)

Monadic style can encode continuation-passing style:

Just use the continuation monad.

But the converse is also true in a certain sense that was discovered by A. Filinski, who invented the notion of "monadic reflection". Here, we exemplify monadic reflection in terms of the list monad, but it is straightforward to abstract over the concrete monad and make this work for every monad.

The list monad operations

(define (return x) (list x))
(define (bind m f)
  (apply append (map f m)))

Monadic reflection (Filinski)

; List[A] -> A (quasi-type)
(define (reflect m)
  (shift k (bind m k)))

; A -> List[A] (quasi-type)
; this is a macro that transforms (reify e) into (reify-thunk (thunk e))
; its sole purpose is to prevent the evaluation of e and wrap it into a thunk
(define-syntaxes (reify)
     (syntax-rules ()
       [(_ e)
        (reify-thunk (thunk e))]))

(define (reify-thunk t)
  (reset (return (t))))

Now we can write direct-style programs (in this case: a direct-style + function), yet use monadic features.

(reify (+ (reflect (list 1 2)) (reflect (list 3 4))))

More information in "Representing Monads" by A. Filinski.

Example: Backtracking using monadic reflection: The n-queens problem. If partial results are desired, lists should be replaced by streams.

(define (fail) (reflect empty))

; Nat List[Nat] Nat -> Bool
(define (safe x l n)
  (or (empty? l)
      (let ((c (first l)))
        (and (not (= x c))
             (not (= x (+ c n)))
             (not (= x (- c n)))
             (safe x (rest l) (+ n 1))))))

(define (queens n)
  (foldl (lambda (_ y)
           (let ((next (reflect (inclusive-range 1 n))))
             (if (safe next y 1)
                 (cons next y)
                 (fail))))
         empty
         (inclusive-range 1 n)))

(reify (queens 8))

Defunctionalization, Refunctionalization

The content of this chapter is available as a Scala file here.

In the discussion of syntactic interpretation vs. meta interpretation we have learned that we only learn something about (and control) a language feature if we choose syntactic interpretation.

Today we want to discuss techniques with which we can make our interpreter so syntactic that it corresponds to an abstract machine: A machine with a (possibly infinite) set of states and a simple transition relation between the states. We already know the technique with which we can take control over the call stack management: CPS transformation. After CPS-transforming the interpreter, we do not rely on the order of evaluation and call stack management of the meta-language anymore. We replicate its definition here:

enum Exp:
  case Num(n: Int)
  case Id(name: String)
  case Add(lhs: Exp, rhs: Exp)
  case Fun(param: String, body: Exp)
  case Ap(funExpr: Exp, argExpr: Exp)

object Exp:
  implicit def num2exp(n: Int): Exp = Num(n)
  implicit def id2exp(s: String): Exp = Id(s)

import Exp._

sealed abstract class Value
type Env = Map[String, Value]
case class NumV(n: Int) extends Value
case class ClosureV(f: Fun, env: Env) extends Value

object CPSTransformed {
  def eval[T](e: Exp, env: Env, k: Value => T): T = e match {
    case Num(n: Int) => k(NumV(n))
    case Id(x) => k(env(x))
    case Add(l, r) =>
      eval(l, env,
           lv => eval(r, env,
                      rv => (lv, rv) match {
                          case (NumV(v1), NumV(v2)) => k(NumV(v1 + v2))
                          case _ => sys.error("can only add numbers")
                        }))
    case f@Fun(param, body) => k(ClosureV(f, env))
    case Ap(f, a) =>
      eval(f, env,
           cl => cl match {
              case ClosureV(f, closureEnv) =>
                eval(a, env,
                     av => eval(f.body, closureEnv + (f.param -> av), k))
              case _ => sys.error("can only apply functions")
            })
  }
}

However, the CPS-transformed interpreter still uses high-level features of the meta-language, most notably first-class functions. We will now introduce one transformation that can be used to transform a program using higher-order functions into one using only first-order functions. It is a general program transformation technique, not restricted only to interpreters.

Lambda Lifting

The first of these techniques is lambda lifting. The goal of lambda lifting is to turn local functions into top-level functions. That is, all "lambdas" only occur at the top-level. Variables in the local environment that are normally stored in the function's closure are instead passed as parameters to the top-level function. Lambda lifting is accomplished by the following steps:

  1. Invent a new and unique name for each function that is not a top-level function.
  2. Create a function with this name. Its body is the body of the former local function. Such a function will contain free variables.
  3. Add a parameter to the so-obtained top-level function for each free variable in its body. Hence, it becomes a higher-order function that returns a function when passed these arguments.
  4. Replace the local function by a call to the new top-level function and pass the corresponding local context via the arguments created in step 3.

Example: Let's lambda-lift the functions y => y + n and y => y * n in

def map(f: Int => Int, xs: List[Int]): List[Int] = xs match {
  case Nil => Nil
  case (x :: xs) => f(x) :: map(f, xs)
}

def addAndMultNToList(n: Int, xs: List[Int]) = map(y => y * n, map(y => y + n, xs))

We create two new top-level functions, let's call them f and g. Their bodies are respectively y => y + n and y => y * n. We add a parameter for each free variable. In the example, the free variable is n in both cases:

def fLam(n: Int) = (y: Int) => y + n
def gLam(n: Int) = (y: Int) => y * n

or shorter:

def f(n: Int)(y: Int) = y + n
def g(n: Int)(y: Int) = y * n

The local function can now be replaced by a call to the new global function.

def addAndMultNToListLifted(n: Int, xs: List[Int]) = map(g(n)(_), map(f(n)(_), xs))

Let's now perform the same technique to the CPS-transformed interpreter given above. It contains local functions in four places: two in the Add branch and two in the Ap branch. We call the corresponding top-level functions, from left to right, addC1, addC2, apC1 and apC2.

An interesting novelty in the interpreter is that some local functions (corresponding to addC1 and apC1) create local functions themselves. This means that addC1 must call addC2 and apC1 must call apC2. The rest of the transformation is a straightforward application of the transformation steps described above:

object LambdaLifted {
  def addC1[T](r: Exp, env: Env, k: Value => T)(lv: Value) =
    eval(r, env, addC2(lv, k))

  def addC2[T](lv: Value, k: Value => T)(rv: Value) = (lv, rv) match {
    case (NumV(v1), NumV(v2)) => k(NumV(v1 + v2))
    case _ => sys.error("can only add numbers")
  }

  def apC1[T](a: Exp, env: Env, k: Value => T)(cl: Value) = cl match {
    case ClosureV(f, closureEnv) => eval(a, env, apC2(f, closureEnv, k))
    case _ => sys.error("can only apply functions")
  }

  def apC2[T](f: Fun, closureEnv: Env, k: Value => T)(av: Value) =
    eval(f.body, closureEnv + (f.param -> av), k)

  def eval[T](e: Exp, env: Env, k: Value => T): T = e match {
    case Num(n: Int) => k(NumV(n))
    case Id(x) => k(env(x))
    case Add(l, r) => eval(l, env, addC1(r, env, k))
    case f@Fun(param, body) => k(ClosureV(f, env))
    case Ap(f, a) => eval(f, env, apC1(a, env, k))
  }
}

The lambda-lifted interpreter contains no local functions anymore, but it still contains higher-order functions, since addC1 etc. return functions that are passed as parameters to other functions.

Defunctionalization

Defunctionalization is a program transformation technique that turns higher-order programs that have already been lambda-lifted into first-order programs that contain no higher-order functions anymore. Any program contains only finitely many function definitions. The idea of defunctionalization is to assign a unique identifier to each of these function definitions. The function-"dispatch" then happens in a function apply, which receives the identifier corresponding to a function definition and dispatches the identifier to the right function body. Every function application within the program is then replaced by a call to the apply function with the function identifier as the first argument.

In addition to the unique identifier, the apply function also needs bindings for the free variables in the function body. Hence we need to store the values for these free variables along with the unique identifier. Finally, the apply function needs to know about the arguments to the function. These become additional parameters of the apply function. Let's illustrate defunctionalization in the addAndMultNToList example from above.

enum FunctionValue:
  case F(n: Int)
  case G(n: Int)

import FunctionValue._

def apply(f: FunctionValue, y: Int): Int = f match {
  case F(n) => y + n
  case G(n) => y * n
}

def map(f: FunctionValue, xs: List[Int]): List[Int] = xs match {
  case Nil => Nil
  case (x :: xs) => apply(f, x) :: map(f, xs)
}

def addAndMultNToListDefun(n: Int, xs: List[Int]) = map(G(n), map(F(n), xs))

Let's now apply defunctionalization to our CPS-transformed interpreter:

object Defunctionalized {

  enum FunctionValue[T]:
    case AddC1(r: Exp, env: Env, k: FunctionValue[T]) extends FunctionValue[T]
    case AddC2(lv: Value, k: FunctionValue[T]) extends FunctionValue[T]
    case ApC1(a: Exp, env: Env, k: FunctionValue[T]) extends FunctionValue[T]
    case ApC2(f: Fun, closureEnv: Env, k: FunctionValue[T]) extends FunctionValue[T]

  import FunctionValue._

  def apply[T](fv: FunctionValue[T], v: Value): T  = fv match {
    case AddC1(r, env, k) => eval(r, env, AddC2(v, k))
    case AddC2(lv, k) => (lv, v) match {
      case (NumV(v1), NumV(v2)) => apply(k, NumV(v1 + v2))
      case _ => sys.error("can only add numbers")
    }
    case ApC1(a, env, k) => v match {
      case ClosureV(f, closureEnv) => eval(a, env, ApC2(f, closureEnv, k))
      case _ => sys.error("can only apply functions")
    }
    case ApC2(f, closureEnv, k) => eval(f.body, closureEnv + (f.param -> v), k)
  }

  def eval[T](e: Exp, env: Env, k: FunctionValue[T]): T = e match {
    case Num(n: Int) => apply(k, NumV(n))
    case Id(x) => apply(k, env(x))
    case Add(l, r) => eval(l, env, AddC1(r, env, k))
    case f@Fun(param, body) => apply(k, ClosureV(f, env))
    case Ap(f, a) => eval(f, env, ApC1(a, env, k))
  }
}

This interpreter can be seen as an abstract machine. The state space of the abstract machine is (Exp \( \times \) Env \( \times \) FunctionValue) \( \cup \) (FunctionValue \( \times \) Value), where \( \times \) stands for cross product and \( \cup \) stands for set union. Every case in the pattern matches in apply and eval can be read as a transition in this state space.

From Interpreter to Abstract Machine

To see that we can read the above functions as transitions of an abstract machine, let's actually construct that machine. Its domain of States is the type MachineState[T]. The final state of the machine is Done(v), for some value v. Its transition function is the function named transition. We can see every intermediate state of the abstract machine. Its state is fully described by the (first-order) state type. We are no longer dependent on call-stack management or higher-order functions of the meta-language (Scala).

object AbstractMachine {
  enum FunctionValue[T]:
    case AddC1(r: Exp, env: Env, k: FunctionValue[T]) extends FunctionValue[T]
    case AddC2(lv: Value, k: FunctionValue[T]) extends FunctionValue[T]
    case ApC1(a: Exp, env: Env, k: FunctionValue[T]) extends FunctionValue[T]
    case ApC2(f: Fun, closureEnv: Env, k: FunctionValue[T]) extends FunctionValue[T]
    case IdentityFV() extends FunctionValue[Value]

  import FunctionValue._

  enum MachineState[T]:
    case EvalState(e: Exp, env: Env, fv: FunctionValue[T]) extends MachineState[T]
    case ApplyState(fv: FunctionValue[T], v: Value) extends MachineState[T]
    case Done(v: Value) extends MachineState[Value]

  import MachineState._

  def transition[T](s: MachineState[T]): MachineState[T] =
    s match {
      case EvalState(e, env, k) => transitionEval(e, env, k)
      case ApplyState(fv, v) => transitionApply(fv, v)
      case Done(v) => sys.error("already done")
    }

  def transitionEval[T](e: Exp, env: Env, k: FunctionValue[T]): MachineState[T] = e match {
    case Num(n: Int) => ApplyState(k, NumV(n))
    case Id(x) => ApplyState(k, env(x))
    case Add(l, r) =>
      EvalState(l, env, AddC1(r, env, k))
    case f@Fun(param, body) => ApplyState(k, ClosureV(f, env))
    case Ap(f, a) =>  EvalState(f, env, ApC1(a, env, k))
   }

   def transitionApply[T](fv: FunctionValue[T], v: Value): MachineState[T] = fv match {
    case IdentityFV() => Done(v)
    case AddC1(r, env, k) => EvalState(r, env, AddC2(v, k))
    case AddC2(lv, k) => (lv, v) match {
          case (NumV(v1), NumV(v2)) => ApplyState(k, NumV(v1 + v2))
          case _ => sys.error("can only add numbers")
         }
    case ApC1(a, env, k) => v match {
        case ClosureV(f, closureEnv) => EvalState(a, env, ApC2(f, closureEnv, k))
        case _ => sys.error("can only apply functions")
    }
    case ApC2(f, closureEnv, k) => EvalState(f.body, closureEnv + (f.param -> v), k)
  }
}
import AbstractMachine._
import FunctionValue._
import MachineState._

Now let's try this out with a concrete example and look at the tract of transitions

val test = Ap(Fun("x", Add("x", 1)), 5)
// test: Exp = Ap(
//   funExpr = Fun(param = "x", body = Add(lhs = Id(name = "x"), rhs = Num(n = 1))),
//   argExpr = Num(n = 5)
// )
val initMS: MachineState[Value] = EvalState(test,
                                            Map.empty,
                                            AbstractMachine.FunctionValue.IdentityFV())
// initMS: MachineState[Value] = EvalState(
//   e = Ap(
//     funExpr = Fun(
//       param = "x",
//       body = Add(lhs = Id(name = "x"), rhs = Num(n = 1))
//     ),
//     argExpr = Num(n = 5)
//   ),
//   env = Map(),
//   fv = IdentityFV()
// )
val s1 = transition(initMS)
// s1: MachineState[Value] = EvalState(
//   e = Fun(param = "x", body = Add(lhs = Id(name = "x"), rhs = Num(n = 1))),
//   env = Map(),
//   fv = ApC1(a = Num(n = 5), env = Map(), k = IdentityFV())
// )
val s2 = transition(s1)
// s2: MachineState[Value] = ApplyState(
//   fv = ApC1(a = Num(n = 5), env = Map(), k = IdentityFV()),
//   v = ClosureV(
//     f = Fun(param = "x", body = Add(lhs = Id(name = "x"), rhs = Num(n = 1))),
//     env = Map()
//   )
// )
val s3 = transition(s2)
// s3: MachineState[Value] = EvalState(
//   e = Num(n = 5),
//   env = Map(),
//   fv = ApC2(
//     f = Fun(param = "x", body = Add(lhs = Id(name = "x"), rhs = Num(n = 1))),
//     closureEnv = Map(),
//     k = IdentityFV()
//   )
// )
val s4 = transition(s3)
// s4: MachineState[Value] = ApplyState(
//   fv = ApC2(
//     f = Fun(param = "x", body = Add(lhs = Id(name = "x"), rhs = Num(n = 1))),
//     closureEnv = Map(),
//     k = IdentityFV()
//   ),
//   v = NumV(n = 5)
// )
val s5 = transition(s4)
// s5: MachineState[Value] = EvalState(
//   e = Add(lhs = Id(name = "x"), rhs = Num(n = 1)),
//   env = Map("x" -> NumV(n = 5)),
//   fv = IdentityFV()
// )
val s6 = transition(s5)
// s6: MachineState[Value] = EvalState(
//   e = Id(name = "x"),
//   env = Map("x" -> NumV(n = 5)),
//   fv = AddC1(r = Num(n = 1), env = Map("x" -> NumV(n = 5)), k = IdentityFV())
// )
val s7 = transition(s6)
// s7: MachineState[Value] = ApplyState(
//   fv = AddC1(r = Num(n = 1), env = Map("x" -> NumV(n = 5)), k = IdentityFV()),
//   v = NumV(n = 5)
// )
val s8 = transition(s7)
// s8: MachineState[Value] = EvalState(
//   e = Num(n = 1),
//   env = Map("x" -> NumV(n = 5)),
//   fv = AddC2(lv = NumV(n = 5), k = IdentityFV())
// )
val s9 = transition(s8)
// s9: MachineState[Value] = ApplyState(
//   fv = AddC2(lv = NumV(n = 5), k = IdentityFV()),
//   v = NumV(n = 1)
// )
val s10 = transition(s9)
// s10: MachineState[Value] = ApplyState(fv = IdentityFV(), v = NumV(n = 6))
val s11 = transition(s10)
// s11: MachineState[Value] = Done(v = NumV(n = 6))

We can also automate this into a function that collects the list of all states.

def evalMachine(e: Exp): List[MachineState[Value]] = {
  val initMS: MachineState[Value] = EvalState(e,
                                              Map.empty,
                                              AbstractMachine.FunctionValue.IdentityFV())
  List.unfold(initMS)({ case Done(v) => None
                        case s => { val s2 = transition(s); Some((s, s2))}})
}

val q = evalMachine(test)
// q: List[MachineState[Value]] = List(
//   EvalState(
//     e = Ap(
//       funExpr = Fun(
//         param = "x",
//         body = Add(lhs = Id(name = "x"), rhs = Num(n = 1))
//       ),
//       argExpr = Num(n = 5)
//     ),
//     env = Map(),
//     fv = IdentityFV()
//   ),
//   EvalState(
//     e = Fun(param = "x", body = Add(lhs = Id(name = "x"), rhs = Num(n = 1))),
//     env = Map(),
//     fv = ApC1(a = Num(n = 5), env = Map(), k = IdentityFV())
//   ),
//   ApplyState(
//     fv = ApC1(a = Num(n = 5), env = Map(), k = IdentityFV()),
//     v = ClosureV(
//       f = Fun(param = "x", body = Add(lhs = Id(name = "x"), rhs = Num(n = 1))),
//       env = Map()
//     )
//   ),
//   EvalState(
//     e = Num(n = 5),
//     env = Map(),
//     fv = ApC2(
//       f = Fun(param = "x", body = Add(lhs = Id(name = "x"), rhs = Num(n = 1))),
//       closureEnv = Map(),
//       k = IdentityFV()
//     )
//   ),
//   ApplyState(
//     fv = ApC2(
//       f = Fun(param = "x", body = Add(lhs = Id(name = "x"), rhs = Num(n = 1))),
//       closureEnv = Map(),
//       k = IdentityFV()
//     ),
//     v = NumV(n = 5)
//   ),
//   EvalState(
//     e = Add(lhs = Id(name = "x"), rhs = Num(n = 1)),
//     env = Map("x" -> NumV(n = 5)),
//     fv = IdentityFV()
//   ),
//   EvalState(
//     e = Id(name = "x"),
//     env = Map("x" -> NumV(n = 5)),
// ...

Refunctionalization

Can we also invert defunctionalization? That is, can we represent a program such that a data type is replaced by a function type?

Let's look at an example.

enum MyList[T]:
  case MyEmptyList() extends MyList[T]
  case MyCons(x: T, xs: MyList[T]) extends MyList[T]

import MyList._

def nth[T](l: MyList[T], i: Int): T = l match {
  case MyEmptyList() => sys.error("index out of range")
  case MyCons(x, xs) => if (i == 0) then x else nth(xs, i - 1)
}

Refunctionalization works smoothly when there is just one pattern match on a data type. In this case, we represent a list by "its" nth function:

type MyListR[T] = Int => T

def myEmptyList[T]: MyListR[T] = _ => sys.error("index out of range")
def myCons[T](x: T,  xs: MyListR[T]): MyListR[T] =
  i => if (i == 0) then x else xs(i - 1)

The defunctionalized and the refunctionalized lists differ in their modular structure. In the defunctionalized version, it is "easy" (only addition of code but no modification of existing code necessary) to add new functions that pattern-match on lists, such as a lengthfunction.

def length[T](l: MyList[T]): Int = l match {
  case MyEmptyList() => 0
  case MyCons(x, xs) => 1 + length(xs)
}

In the refunctionalized version, on the other hand, it is "easy" to add new ways to create lists. For instance, here is a constructor for the infinite list of numbers

def allNats: MyListR[Int] = i => i

The two versions hence corresponds to the two dimensions of the aforementioned "expression problem". These program transformations are hence generally applicable program transformations in a programmer's modularity toolbox.

But what about refunctionalization in the case that there is more than one pattern-match on the data type? It turns out that we can refunctionalize if we generalize functions to objects, that is, if we view functions as special kinds of objects with a single apply method.

For instance, in the case of lists with two pattern-matching functions, nth and length, we can represent lists as objects with two methods, one for each pattern match.

trait MyListRO[T] {
  def nth(i: Int): T
  def length: Int
}

def myEmptyListO[T]: MyListRO[T] = new MyListRO[T] {
  def nth(i: Int): T = sys.error("index out of range")
  def length: Int = 0
}

def myConsO[T](x: T,  xs: MyListRO[T]): MyListRO[T] = new MyListRO[T] {
  def nth(i: Int): T = if (i == 0) then x else xs.nth(i - 1)
  def length: Int =  1 + xs.length
}

With this generalization, we can defunctionalize any object type (the transformation could then maybe be called "deobjectionalization") and we can refunctionalize any algebraic data type, and thereby completely invert the extensibility of the program.

Recall that both transformations are global and not compositional, that is, the full program must be transformed at once.

We have glossed over some technical details that would need to be addressed to automate these transformations and make them inverse to each other. For instance, we have not addressed how to reverse the lambda lifting part.

Historical notes: Defunctionalization was proposed by John Reynolds in 1972 in his landmark paper "Definitional Interpreters for Higher-Order Programming Languages". Similar to these lecture notes, Reynolds applied defunctionalization to a CPS-transformed interpreter. The generalization of refunctionalization to use objects instead of functions was presented in a 2015 paper by Rendel, Trieflinger, and Ostermann entitled "Automatic Refunctionalization to a Language with Copattern Matching: With Applications to the Expression Problem". A fully formal account of the transformations that also addresses invertible lambda lifting and proves that the transformations are inverses of each other can be found in the 2020 paper "Decomposition Diversity with Symmetric Data and Codata" by Binder, Jabs, Skupin and Ostermann.

Type Systems

The content of this chapter is available as a Scala file here.

Type Systems

The aim of type systems is to prevent certain classes of errors to occur at runtime. Examples of such errors include the addition of a number to a function, or the usage of a free variable. There can be many different type systems for the same programming language. A sound type systems guarantees that the errors that the type system aims to prevent will not occur in any run of the program. This is different from testing, in which only a single program run on some particular input data is considered.

Since - by Rice's theorem - all interesting properties of the runtime behavior of a program (in a Turing complete language) are undecidable, type systems are necessarily imperfect. But they are usually designed in such a way that the errors are directed:

Each type system unfairly rejects some programs that could be executed without runtime errors. But programs that are accepted by the type system will never produce one of the precluded runtime errors. The latter property is usually called soundness, the former property completeness. Hence we can say that most type systems are sound but not complete. One also often says that type systems are conservative. A conservative approximation of the behavior of a program is one that considers all possible program runs but also some impossible program runs - an overapproximation.

Well-typedness is usually a context-sensitive property, hence it cannot be expressed in the (context-free) grammar of the language but is defined as an additional filter which rules out some syntactically correct programs. Let's look at a small toy language to illustrate type systems:

enum Exp:
  case Num(n: Int)
  case Add(lhs: Exp, rhs: Exp)
  case Bool(x: Boolean)
  case If(cond: Exp, thn: Exp, els: Exp)

object Exp:
  implicit def num2exp(n: Int): Exp = Num(n)
  implicit def bool2exp(x: Boolean): Exp = Bool(x)

import Exp._

def eval(e: Exp): Exp = e match {
  case Add(l, r) => (eval(l), eval(r)) match {
    case (Num(x), Num(y)) => Num(x + y)
    case _ => sys.error("can only add numbers")
   }
  case If(cond, thn, els) => eval(cond) match {
      case Bool(true) => eval(thn)
      case Bool(false) => eval(els)
      case _ => sys.error("Condition must be boolean")
    }
  case _ => e
}

In this language, we can see that two different types of runtime errors can occur: An addition where one of the operands is not a number, or an if-expression where the condition does not evaluate to a boolean.

Here are a few syntactically correct programs. Only the first three execute correctly. The other ones yield runtime errors.

val ex1 = Add(3, 5)
val ex2 = If(true, 7, 5)
val ex2b = If(true, 7, true)
val ex3 = If(Add(3, 4), 7, true)
val ex4 = Add(true, 3)

Let's now design a type system for this language. Most type systems entail a classification of the values into different types. In this type system, we choose to define two types, one called BoolType and one called IntType.

enum Type:
  case BoolType()
  case IntType()
import Type._

A type checker is a compositional assignment of types (or type errors) to expressions. Compositionality means that the type of a composite expression is computed from the type of its subexpressions. Compositionality is quite important for reasoning about types. In this case, the type checker is a rather straightforward structural recursion (= compositionality) over expressions.

def typeCheck(e: Exp): Type = e match {
  case Num(n) => IntType()
  case Bool(x) => BoolType()
  case Add(a, b) => (typeCheck(a), typeCheck(b)) match {
    case (IntType(), IntType()) => IntType()
    case _ => sys.error("Type Error in addition")
  }
  case If(c, t, e) => (typeCheck(c), typeCheck(t), typeCheck(e)) match {
    case (BoolType(), t1, t2) => if (t1 == t2) t1 else sys.error("Type error in if")
    case _ => sys.error("Type error in If")
  }
}

Type Soundness

If we typecheck the examples from above with the algorithm, we can see that it rejects all programs that lead to runtime errors, and it also (unfairly) rejects one program that can be executed without error. Hence the type system is conservative, as expected. We can now say more precisely what we mean by soundness of the type checker, namely that it correctly predicts the kind of value that the interpreter will produce:

If e is an expression and typeCheck(e) yields a type t, then eval(e) yields a value v such that typeCheck(v) == t.

This statement of type soundness has to be changed a bit when we have a programming language with non-terminating programs. Then it will have the following form:

If e is an expression and typeCheck(e) yields a type t, then eval(e) either diverges, or it yields a value v such that typeCheck(v) == t, but it does not terminate with a runtime error.

In the most general case, we also have runtime errors that are not captured by the type system. In this case we have to restrict the last part of the sentence to those runtime errors that are captured by the type system.

Simply Typed Lambda Calculus

The content of this chapter is available as a Scala file here.

The Simply Typed Lambda Calculus

We will now consider the so-called Simply Typed Lambda Calculus (STLC). We start with the untyped substitution-based lambda calculus augmented by the possibility to add type annotations to function definitions. The type annotation is ignored by the interpreter. Why are we using the substitution-based interpreter? Because it is simpler to state the type soundness theorem. If we had values that are separate from expressions, we would need to define a type system for these values. This is particularly tricky for closures with their embedded environments.

We also show a couple of standard extensions to the simply typed lambda calculus:

  1. A unit type JunitType() with associated term JUnit()
  2. Let-bindings (which don't need type annotations)
  3. Type Ascription (corresponding to : in Scala, like 5 + 3: Int)
  4. Products (or Pairs, more specifically)
  5. Sums (binary sums, more specifically)

To avoid the problem of "guessing" the "from" type in a function definition, we annotate function definitions with the expected argument type.

sealed abstract class Type

enum Exp:
  case Num(n: Int)
  case Id(name: String)
  case Add(lhs: Exp, rhs: Exp)
  case Fun(param: String, t: Type, body: Exp)
  case Ap(funExpr: Exp, argExpr: Exp)
  case Junit()
  case Let(x: String, xdef: Exp, body: Exp)
  case TypeAscription(e: Exp, t: Type)

  case Product(e1: Exp, e2: Exp)
  case Fst(e: Exp)
  case Snd(e: Exp)

  case SumLeft(left: Exp, right: Type)
  case SumRight(left: Type, right: Exp)
  case EliminateSum(e: Exp, fl: Exp, fr: Exp)

object Exp:
  implicit def num2exp(n: Int): Exp = Num(n)
  implicit def id2exp(s: String): Exp = Id(s)

import Exp._

def freshName(names: Set[String], default: String): String = {
  var last: Int = 0
  var freshName = default
  while (names contains freshName) { freshName = default + last.toString; last += 1; }
  freshName
}

def freeVars(e: Exp): Set[String] = e match {
   case Id(x) => Set(x)
   case Add(l, r) => freeVars(l) ++ freeVars(r)
   case Fun(x, _, body) => freeVars(body) - x
   case Ap(f, a) => freeVars(f) ++ freeVars(a)
   case Num(n) => Set.empty
   case Junit() => Set.empty
   case TypeAscription(e, t) => freeVars(e)
   case Let(x, xdef, body) => freeVars(xdef) ++ (freeVars(body) - x)
   case Product(e1, e2) => freeVars(e1) ++ freeVars(e2)
   case Fst(e) => freeVars(e)
   case Snd(e) => freeVars(e)
   case SumLeft(e, _) => freeVars(e)
   case SumRight(_, e) => freeVars(e)
   case EliminateSum(e, fl, fr) => freeVars(e) ++ freeVars(fl) ++ freeVars(fr)
}

def subst(e1: Exp, x: String, e2: Exp): Exp = e1 match {
  case Num(n) => e1
  case Junit() => e1
  case Add(l, r) => Add(subst(l, x, e2), subst(r, x, e2))
  case Id(y) => if (x == y) e2 else Id(y)
  case Ap(f, a) => Ap(subst(f, x, e2), subst(a, x, e2))
  case TypeAscription(e, t) => TypeAscription(subst(e, x, e2), t)
  case Fun(param, t, body) =>
    if (param == x) e1 else {
      val fvs = freeVars(body) ++ freeVars(e2)
      val newvar = freshName(fvs, param)
      Fun(newvar, t, subst(subst(body, param, Id(newvar)), x, e2))
    }
  case Let(y, ydef, body) =>
    if (x == y) Let(y, subst(ydef, x, e2), body) else {
      val fvs = freeVars(body) ++ freeVars(e2)
      val newvar = freshName(fvs, y)
      Let(newvar, subst(ydef, x, e2), subst(subst(body, y, Id(newvar)), x, e2))
    }
  case Product(a, b) => Product(subst(a, x, e2), subst(b, x, e2))
  case Fst(e) => Fst(subst(e, x, e2))
  case Snd(e) => Snd(subst(e, x, e2))
  case SumLeft(e, t) => SumLeft(subst(e, x, e2), t)
  case SumRight(t, e) => SumRight(t, subst(e, x, e2))
  case EliminateSum(e, fl, fr) =>
    EliminateSum(subst(e, x, e2), subst(fl, x, e2), subst(fr, x, e2))
}

def eval(e: Exp): Exp = e match {
  case Id(v) => sys.error("unbound identifier: " + v)
  case Add(l, r) => (eval(l), eval(r)) match {
    case (Num(x), Num(y)) => Num(x + y)
    case _ => sys.error("can only add numbers")
  }
  case Ap(f, a) => eval(f) match {
    case Fun(x, _, body) => eval(subst(body, x, eval(a))) // call-by-value
    case _ => sys.error("can only apply functions")
  }
  case TypeAscription(e, _) => eval(e)
  case Let(x, xdef, body) => eval(subst(body, x, eval(xdef)))
  case Product(a, b) => Product(eval(a), eval(b))
  case Fst(e) => eval(e) match {
    case Product(a, b) => a
    case _ => sys.error("can only apply Fst to products")
  }
  case Snd(e) => eval(e) match {
    case Product(a, b) => b
    case _ => sys.error("can only apply Snd to products")
  }
  case SumLeft(e, t) => SumLeft(eval(e), t)
  case SumRight(t, e) => SumRight(t, eval(e))
  case EliminateSum(e, fl, fr) => eval(e) match {
    case SumLeft(e2, _) => eval(Ap(fl, e2))
    case SumRight(_, e2) => eval(Ap(fr, e2))
    case _ => sys.error("can only eliminate sums")
  }
  case _ => e // numbers and functions evaluate to themselves
}

We classify values into three types: Booleans, integers, and function types. For function types, we need some abstraction for its input and output; otherwise the type checker cannot be compositional. Luckily we do already have such an abstraction, namely types. Hence FunType becomes a recursive data type.

case class NumType() extends Type
case class FunType(from: Type, to: Type) extends Type
case class JunitType() extends Type
case class ProductType(fst: Type, snd: Type) extends Type
case class SumType(left: Type, right: Type) extends Type

Let's look at the type checker for the simply typed lambda calculus. To deal with identifiers, we need an abstraction of environments. A type environment has the form Map[String, Type].

The type checker for the STLC is as follows:

def typeCheck(e: Exp, gamma: Map[String, Type]): Type = e match {
  case Num(n) => NumType()
  case Junit() => JunitType()
  case Id(x) => gamma.get(x) match {
    case Some(t) => t
    case _ => sys.error("free variable: " ++ x.toString)
  }
  case Add(l, r) => (typeCheck(l, gamma), typeCheck(r, gamma)) match {
    case (NumType(), NumType()) => NumType()
    case _ => sys.error("Type error in Add")
  }
  case Fun(x, t, body) => FunType(t, typeCheck(body, gamma + (x -> t)))
  case Ap(f, a) => {
    typeCheck(f, gamma) match {
      case FunType(from, to) =>
        if (from == typeCheck(a, gamma))
          to
        else
          sys.error("type error: arg does not match expected type")
      case _ => sys.error("first operand of Ap must be a function")
    }
  }
  case Let(x, xdef, body) => typeCheck(body, gamma + (x -> typeCheck(xdef, gamma)))
  case TypeAscription(e, t) =>
    if (typeCheck(e, gamma) == t) t else sys.error("type error in ascription")
  case Product(e1, e2) => ProductType(typeCheck(e1, gamma), typeCheck(e2, gamma))
  case Fst(e) => typeCheck(e, gamma) match {
    case ProductType(t1, t2) => t1
    case _ => sys.error("can only project Products")
  }
  case Snd(e) => typeCheck(e, gamma) match {
    case ProductType(t1, t2) => t2
    case _ => sys.error("can only project Products")
  }
  case SumLeft(e, t) => SumType(typeCheck(e, gamma), t)
  case SumRight(t, e) => SumType(t, typeCheck(e, gamma))
  case EliminateSum(e, fl, fr) => typeCheck(e, gamma) match {
    case SumType(left, right) => (typeCheck(fl, gamma), typeCheck(fr, gamma)) match {
      case (FunType(lf, t1), FunType(rf, t2)) if ((left == lf) && (right == rf)) =>
        if (t1 == t2)
          t1
        else
          sys.error("type error: functions must have same return type")
      case _ =>
        sys.error("type error in EliminateSum: second and third argument must be functions")
    }
    case _ => sys.error("type error: can only eliminate sums")
  }

}

Soundness of Simply Typed Lambda Calculus (STLC):

If e: Exp and typeCheck(e, Map.empty) == t, then typeCheck(eval(e), Map.empty) == t.

Enable JavaScript to see the quiz

What is the overall type of the following expression?

  Fun("x", NumType(), Product(5, Add(3, "x")))  
    

  FunType(NumType(), NumType())
      
The return type is a product.

  ProductType(NumType(), NumType())
      
This is only the return type of the function.

  FunType(NumType(),
          ProductType(NumType(), NumType()))
      

  NumType()
      
This is only the input type of the function.
What is the overall type of the following expression?

  EliminateSum(SumLeft(3, JunitType()),
               Fun("x", NumType(), "x"),
               Fun("x", JunitType(), 0))
    
JunitType() This is the type of the right-hand side of the sum type. NumType() SumType(NumType(), JunitType()) This is the type of the scrutinee. FunType(NumType(), NumType()) This is the type of the whole left branch. The type of the elimination is the common return type of the branches.

Type Inference

The content of this chapter is available as a Scala file here.

Type inference (sometimes called type reconstruction) is the idea to avoid type annotations in the program and instead infer all typing-related information from the program. For instance, if + is an operator on numbers, then we can infer that x must have the number type in an expression x + 1.

The best-known type inference algorithm is Hindley-Milner type inference with so-called "let-polymorphism". The basic idea of this algorithm is that the type checker synthesizes fresh type variables whenever a type that is not yet known is needed as an input to a recursive call of the type checking algorithm. For instance, in a function definition, a fresh type variable is created for the type of the function argument. In addition to producing a type as output, the type checker will also produce a set of equality constraints between types (containing type variables). Type checking succeeds only if these equality constraints have a solution. "Having a solution" means that there is a substitution (a mapping from type variables to types) that makes all equations trivial. For instance, the substitution that substitutes X by Num and Y by Bool is a solution for the constraints FunType(X, Bool) == FunType(Num, Y).

In the code below we choose a slightly different representation of substitution, namely as a function that performs the substitution (with regard to our discussion of refunctionalization we could say that we refunctionalize the substitution type).

enum Type:
  case FunType(from: Type, to: Type)
  case NumType()
  case TypeVar(x: String)
import Type._

def freeTypeVars(t: Type): Set[String] = t match {
  case FunType(f, t) => freeTypeVars(f) ++ freeTypeVars(t)
  case NumType() => Set.empty
  case TypeVar(x) => Set(x)
}

def substitution(x: String, s: Type) = new Function[Type, Type] {
  def apply(t: Type) = t match {
    case FunType(from, to) => FunType(this(from), this(to))
    case NumType() => NumType()
    case TypeVar(y) => if (x == y) s else TypeVar(y)
  }
}

Note that in the FunType-case this is the substitution itself which is simply applied to the input and output type.

A substitution can be found (if it exists) by an algorithm called the "Robinson unification algorithm":

def unify(eq: List[(Type, Type)]): Type => Type = eq match {
  case Nil => identity _
  case (NumType(), NumType()) :: rest => unify(rest)
  case (FunType(f1, t1), FunType(f2, t2)) :: rest => unify((f1, f2) :: (t1, t2) :: rest)
  case (TypeVar(x), TypeVar(y)) :: rest if x == y => unify(rest)
  case (TypeVar(x), t) :: rest => {
    if (freeTypeVars(t)(x)) sys.error(s"Occurs check: $x occurs in $t")
    val s = substitution(x, t)
    // andThen is just function composition with the first function (here s) applied first
    s.andThen(unify(rest.map(p => (s(p._1), s(p._2)))))
  }
  case (t, TypeVar(x)) :: rest => unify((TypeVar(x), t) :: rest)
  case (t1, t2) :: rest => sys.error(s"Cannot unify $t1 and $t2")
}

It is not easy to see that this algorithm terminates in all cases, but it does (ask yourself: why?). It is both sound and complete. It returns the so-called "most general unifier".

Let us now apply this to a concrete language. Here is its definition:

enum Exp:
  case Num(n: Int)
  case Id(name: String)
  case Add(lhs: Exp, rhs: Exp)
  case Fun(param: String, body: Exp) // No type annotation!
  case Ap(funExpr: Exp, argExpr: Exp)
  case Let(x: String, xdef: Exp, body: Exp)

object Exp:
  implicit def num2exp(n: Int): Exp = Num(n)
  implicit def id2exp(s: String): Exp = Id(s)

import Exp._

def freshName(names: Set[String], default: String): String = {
  var last: Int = 0
  var freshName = default
  while (names contains freshName) { freshName = default + last.toString; last += 1; }
  freshName
}

def freeVars(e: Exp): Set[String] =  e match {
   case Id(x) => Set(x)
   case Add(l, r) => freeVars(l) ++ freeVars(r)
   case Fun(x, body) => freeVars(body) - x
   case Ap(f, a) => freeVars(f) ++ freeVars(a)
   case Num(n) => Set.empty
   case Let(x, xdef, body) => freeVars(xdef) ++ (freeVars(body) - x)
}

def subst(e1: Exp, x: String, e2: Exp): Exp = e1 match {
  case Num(n) => e1
  case Add(l, r) => Add(subst(l, x, e2), subst(r, x, e2))
  case Id(y) => if (x == y) e2 else Id(y)
  case Ap(f, a) => Ap(subst(f, x, e2), subst(a, x, e2))
  case Fun(param, body) =>
    if (param == x) e1 else {
      val fvs = freeVars(body) ++ freeVars(e2)
      val newvar = freshName(fvs, param)
      Fun(newvar, subst(subst(body, param, Id(newvar)), x, e2))
    }
  case Let(y, ydef, body) =>
    if (x == y) Let(y, subst(ydef, x, e2), body) else {
      val fvs = freeVars(body) ++ freeVars(e2)
      val newvar = freshName(fvs, y)
      Let(newvar, subst(ydef, x, e2), subst(subst(body, y, Id(newvar)), x, e2))
    }
}

def eval(e: Exp): Exp = e match {
  case Id(v) => sys.error("unbound identifier: " + v)
  case Add(l, r) => (eval(l), eval(r)) match {
    case (Num(x), Num(y)) => Num(x + y)
    case _ => sys.error("can only add numbers")
  }
  case Ap(f, a) => eval(f) match {
    case Fun(x, body) => eval(subst(body, x, eval(a))) // call-by-value
    case _ => sys.error("can only apply functions")
  }
  case Let(x, xdef, body) => eval(subst(body, x, eval(xdef)))
  case _ => e // numbers and functions evaluate to themselves
}

The type checker returns both a type (possibly containing type variables) and a list of equality constraints. Note that the only way type checking can actually fail is when encountering a free variable.

var tyvCount: Int = 0
def freshTypeVar: TypeVar = {
  tyvCount += 1
  TypeVar("X" + tyvCount.toString)
}

def typeCheck(e: Exp, gamma: Map[String, Type]): (List[(Type, Type)], Type) = e match {
  case Num(n) => (List.empty, NumType())
  case Id(x) => gamma.get(x) match {
    case Some(t) => (List.empty, t)
    case _ => sys.error("free variable: " ++ x.toString)
  }
  case Add(l, r) => (typeCheck(l, gamma), typeCheck(r, gamma)) match {
    case ((eq1, t1), (eq2, t2)) =>
      ((t1, NumType()) :: (t2, NumType()) :: (eq1 ++ eq2), NumType())
  }
  case Fun(x, body) => {
    val xtype = freshTypeVar
    val resbody = typeCheck(body, gamma + (x -> xtype))
    (resbody._1, FunType(xtype, resbody._2))
  }
  case Ap(f, a) => {
    val toType = freshTypeVar
    (typeCheck(f, gamma), typeCheck(a, gamma)) match {
      case ((eqs1, ft), (eqs2, at)) =>
        ((ft, FunType(at, toType)) :: (eqs1 ++ eqs2), toType)
    }
  }
  case Let(x, xdef, body) => {
    // important if x is not used in body
    val (constraints1, _) = typeCheck(xdef, gamma)
    // Let-Polymorphism! type check after substitution
    val (constraints2, typ) = typeCheck(subst(body, x, xdef), gamma)
    (constraints1 ++ constraints2, typ)
  }

}

The Let-case of the type checker implements a special extension of the type checking algorithm, which allows one to use the same definition type-polymorphically at multiple types. This is achieved by first substituting the binding into the body and only then type check the latter. This way, any occurrence of the binding in the body can have a different type. Note further, that we still type check the definition of the binding explicitly, in order to gather the generated constraints also in the case that the binding is not used in the body.

Full type checking is completed only when the constraints have a solution. The resulting substitution is then applied to the returned type to give the overall inferred type. This is captured by the following definition.

def doTypeCheck(e: Exp, gamma: Map[String, Type]) = {
  val (constraints, resType) = typeCheck(e, gamma)
  unify(constraints)(resType)
}

assert(doTypeCheck(42, Map.empty) == NumType())
assert(doTypeCheck(Fun("x", Add("x", 1)), Map.empty) == FunType(NumType(), NumType()))

Properties of type inference

As an instance of let-polymorphism, consider the following example where the identity function is once applied to a function and once to a number:

val exId =
  doTypeCheck(
    Let("id", Fun("x", "x"), Ap(Ap("id", Fun("x", Add("x", 1))), Ap("id", 42))),
    Map.empty)
// exId: Type = NumType()

This function could not be type checked in STLC.

This example, on the other hand, should trigger an occurs check error:

val exOmega = doTypeCheck(Fun("x", Ap("x", "x")), Map.empty)
// java.lang.RuntimeException: Occurs check: X9 occurs in FunType(TypeVar(X9),TypeVar(X10))
// 	at scala.sys.package$.error(package.scala:27)
// 	at repl.MdocSession$MdocApp.unify(type-inference.md:45)
// 	at repl.MdocSession$MdocApp.doTypeCheck(type-inference.md:176)
// 	at repl.MdocSession$MdocApp.$init$$$anonfun$1(type-inference.md:202)
// 	at repl.MdocSession$MdocApp.$init$$$anonfun$adapted$1(type-inference.md:203)

Hence omega cannot be type checked in STLC (and not even with let-polymorphism). STLC has the remarkable property that all well-typed programs terminate.

Type inference satisfies the following completeness property:

If there exist type annotations that make a program type check in the STLC type checker, then the type inference will also be able to type check the non-annotated version of the program.

Further, due to let-polymorphism, with type inference we can also type check some programs that would be ill-formed in STLC.

Moreover, the type system is still sound:

If doTypeCheck(e, Map.empty) == t, then eval(e) == v and doTypeCheck(v) == t (modulo \( \alpha \)-renaming of type variables).

Enable JavaScript to see the quiz

Which of the following pairs of types can be unified? FunType(NumType(), NumType())   and FunType(TypeVar("X"), TypeVar("X")) FunType(TypeVar("X"), TypeVar("Y"))   and NumType() A function type can never be unified with NumType(). FunType(NumType(), TypeVar("X"))   and FunType(NumType(), TypeVar("Y")) FunType(NumType(), TypeVar("Y"))   and TypeVar("X")