Fork me on GitHub

This is a substitute lecture by Yi Dai for the course //Programming Languages and Types// by Klaus Ostermann at the University of Marburg.

The material in this lecture is loosely based on the following two papers:

  • Mads Sig Ager, Dariusz Biernacki, Olivier Danvy, and Jan Midtgaard. A functional correspondence between evaluators and abstract machines. In Dale Miller, editor, Proceedings of the Fifth ACM-SIGPLAN International Conference on Principlesand Practice of Declarative Programming (PPDP'03), pages 8-19. ACM Press, August 2003.

  • Olivier Danvy. On evaluation contexts, continuations, and the rest of the computation. In Hayo Thielecke, editor, Proceedings of the Fourth ACM SIGPLAN Workshop on Continuations, Technical report CSR-04-1, Department of Computer Science, Queen Mary’s College, pages 13-23, Venice, Italy, January 2004. Invited talk.

This lecture consists of two parts. The first part goes from evaluators to abstract machines. The second part goes the other way around. In the first part, our object-language is the call-by-name lambda-calculus. In the second part, our object-language is the call-by-value lambda-calculus. For both parts, we use the de-Bruijn-index notation for lambda-expressions. (Please find more about de-Bruijn-index notation on Wikipedia.) Our meta-language is Scala as usual.

From Evaluators to Abstract Machines

In this part, we will look at how to derive an abstract machine, called Krivine’s machine, for the call-by-name lambda-calculus, from a standard interpreter by a series of program transformations.

Higher-Order Function

This interpreter implements the call-by-name lambda-calculus. It features meta-level higher-order functions in the following two senses:

  1. Object-level first-class functions are implemented by meta-level first-class functions and object-level function application is implemented by meta-level function application. Since object-level functions are higher-order, meta-level functions implementing them must also be higher-order.
  2. Since eval returns such meta-level functions as results, eval is higher-order too.
object HOF {
  sealed abstract class Exp
  sealed abstract class Value

  case class Id(ind: Int) extends Exp
  case class Fun(body: Exp) extends Exp
  case class App(funExpr: Exp, argExpr: Exp) extends Exp

  case class Proc(fun: Value => Value) extends Value

  type Env = List[Value]

  def eval(exp: Exp, env: Env): Value = exp match {
    case Id(ind) => env(ind)
    case Fun(body) => Proc(arg => eval(body, arg :: env))
    case App(funExpr, argExpr) => eval(funExpr, env) match {
      case Proc(fun) => fun(eval(argExpr, env))
    }
  }

  def interpret(exp: Exp): Value = eval(exp, Nil)
}

Note that in the implementation of HOF, the evaluation order of the object-language depends on the evaluation order of the meta-language. The dependence manifests at fun(eval(argExpr, env)).

On one hand, if the meta-language uses call-by-value, which happens to be so for Scala, the object-language is forced to be call-by-value; if instead the meta-language uses call-by-name, the object-language becomes call-by-name too.

On the other hand, if the meta-language uses call-by-value but we want the object-languge to be call-by-name, a special construct that can delay evaluation must be introduced into the meta-language; if the meta-language uses call-by-name but we want the object-language to be call-by-value, a special construct that can force evaluation must be introduced into the meta-language. In either case, the meta-language must be extended, which sometimes may be difficult or impossible.

Fortunately a program transformation technique can solve both problems gracefully, eliminating the evaluation-order dependence between the object-language and meta-language, that is, by transforming the interpreter into continuation-passing style.

But to make life a bit easier, let us first get rid of meta-level first-class functions that implement object-level first-class functions.

Closure Conversion

Closure conversion converts the representation of object-level first-class functions to meta-level data structures, namely closures. The consequences are twofold:

  1. It eliminates the use of meta-level first-class functions to represent object-level first-class functions.

  2. It turns meta-level higher-order functions back to first-order, particularly eval.

Keeping from meta-level first-class functions but with meta-level first-order functions opens more choices of meta-languages for implementation. For example, a language like C that does not natively support first-class functions can be used.

After closure conversion, we have an interpreter in direct style, that is, not manipulating explicit continuations.

object HOF_CC {
  sealed abstract class Exp
  sealed abstract class Value

  case class Id(ind: Int) extends Exp
  case class Fun(body: Exp) extends Exp
  case class App(funExpr: Exp, argExpr: Exp) extends Exp

  type Env = List[Value]

  case class ClosureV(body: Exp, env: Env) extends Value

  def eval(exp: Exp, env: Env): Value = exp match {
    case Id(ind) => env(ind)
    case Fun(body) => ClosureV(body, env)
    case App(funExpr, argExpr) => eval(funExpr, env) match {
      case ClosureV(body, closureEnv) => eval(body, eval(argExpr, env) :: closureEnv)
    }
  }

  def interpret(exp: Exp): Value = eval(exp, Nil)
}

Call-by-Name CPS-Transformation

The problem of evaluation-order dependence between the object-language and the meta-language remains in the interpreter in direct style after closure conversion, as evidenced by eval(body, eval(argExpr, env) :: closureEnv). To solve the problem without extending the meta-language, we must turn to CPS-transformation.

As to see how to implement a call-by-name language in a call-by-value language without extending the meta-language, we decide our object-language to be the call-by-name lambda-calculus and use the call-by-name CPS-transformation to transform the interpreter from direct style into continuation-passing style.

The result of the transformation has the following features:

  1. The evaluation order of the object-language is call-by-name. It no longer depends on the evaluation order of the meta-language. Even if someday Scala switches to call-by-name, our interpreter still correctly implements the call-by-name lambda-calculus.

  2. The whole interpreter is tail recursive and can readily be tail-call optimized.

object HOF_CC_CbNCPS {
  sealed abstract class Exp
  sealed abstract class Value
  sealed abstract class Computation

  case class Id(ind: Int) extends Exp
  case class Fun(body: Exp) extends Exp
  case class App(funExpr: Exp, argExpr: Exp) extends Exp

  type Env = List[Computation]

  case class ClosureV(body: Exp, env: Env) extends Value

  type Cont = Value => Value

  case class Thunk(fun: Cont => Value) extends Computation

  def eval(exp: Exp, env: Env, k: Cont): Value = exp match {
    case Id(ind) => env(ind) match {
      case Thunk(fun) => fun(k)
    }
    case Fun(body) => k(ClosureV(body, env))
    case App(funExpr, argExpr) =>
      eval(funExpr, env, v => v match {
        case ClosureV(body, closureEnv) =>
          eval(body, Thunk(k => eval(argExpr, env, k)) :: closureEnv, k)
      })
  }

  def interpret(exp: Exp): Value = eval(exp, Nil, v => v)
}

Defunctionalization

Defunctionalization is a general program transformation technique that eliminates higher-order functions, by replacing the creation of first-class functions with the construction of data structures and the application of first-class functions with the destruction of data structures.

A program that contains higher-order functions can be defunctionalized following three steps:

  1. Encoding — Identify first-class functions in the program and classify them according to their types. For each function type, introduce an encoding data type. For each function instance (i.e. function of the encoded function type) introduce a constructor that will construct data of the encoding type. Make sure that each data constructor takes arguments that have the same types as the free variables of the function instance. The idea is that each constructor should save the values bound to the free variables of the function instance. Encoding should not cause any informatoin loss.

  2. Decoding — For each encoding data type, define a function (conventionally called “apply”) that takes one argument of the data type and the other of the argument type of the encoded function type. This “apply” function will use pattern matching to do case analysis on its first argument in order to extract the saved values bound to the free variables of the encoded function instance. The right-hand side of each case will be the body of the encoded function instance, with variables renamed accordingly.

  3. Refactoring — Go through the program. When an encoded function type is met, replace it with its encoding data type. When a function instance is met, encode it by constructing data from its free variables using the corresponding constructor. In this way, values bound to the free variables of the function instance are saved. Locate all the places where a function instance is applied, replacing its application with an explict call of the “apply” function on (the designator of) the function instance and its argument.

We illustrate how to defunctionalize a higher-order program following these two steps by an example. Below is a simple higher-order program that defines a function that will first add n to and then multiply by n every integer in a list xs.

def map(f: Int => Int, xs: List[Int]): List[Int] = xs match {
  case Nil => Nil
  case x :: xs => f(x) :: map(f, xs)
}

def addMulInts(n: Int, xs: List[Int]): List[Int] =
  map(x => x * n, map(x => x + n, xs))

Let us apply the three-step procedure to this program.

1. Encoding

There are two first-class functions in the program, namely x = > x * n and x => x + n. Both are instances of the type Int => Int. Therefore we only need to introduce one data type, call it IntF. Both functions also have n of type Int as their free variables. So we introduce two data constructors, call them AddN and MulN respectively. Each takes an argument n of type Int.

sealed abstract class IntF
case class AddN(n: Int) extends IntF
case class MulN(n: Int) extends IntF

2. Decoding

Next we define the “apply” function which takes one argument of type IntF and the other argument of the argument type of the encoded function type Int => Int, that is, Int. The “apply” function will distinguish two cases, MulN(n) or AddN(n), and respectively add n to or multiply by n its second argument. Note that we use the same names (“x” and “n” respectively) for the second parameter and the pattern variable of each case in apply, as those for the parameter and free variable in each encoded function instance, so that we are not concerned with variable renaming and can put the body of the encoded function instance as is directly to the right-hand side of each case. But normally, renaming variables in the body of the encoded function instance is required, for instance, if the second function instance in our example program is y => y * n instead of x => x * n.

def apply(f: IntF, x: Int): Int = f match {
  case MulN(n) => x * n
  case AddN(n) => x + n
}

3. Refactoring

Now we are ready to actually defunctionalize the original higher-order program. First, we replace the function type Int => Int of the parameter f of map with our encoding data type IntF. Second, we replace the two function instances x => x * n and x => x + n in the body of AddMulInts by MulN(n) and AddN(n) respectively. Note that both functions will be bound to f and passed in to map. In other words, where f is applied in map is where these function instances are applied. Thus the third is to replace the application of f to x with an explicit invocation of apply on f and x.

def map(f: IntF, xs: List[Int]): List[Int] = xs match {
  case Nil => Nil
  case x :: xs => apply(f, x) :: map(f, xs)
}

def addMulInts(n : Int, xs : List[Int]) : List[Int] =
  map(MulN(n), map(AddN(n), xs))
 

Defunctionalization can also be applied to non-trivial higher-order programs, like interpreters.

Actually closure conversion can be seen as a special case of defunctionalization. It is usually done before transforming the interpreter into continuation-passing style to ease the whole task. Below is what we get by defunctionalizing meta-level first-class functions that implement object-level first-class functions. applyClosure is the “apply” function. It can be easily verified that inlining applyClosure gives exactly the interpreter defined in HOF_CC.

object HOF_DFP {
  sealed abstract class Exp
  sealed abstract class Value

  case class Id(ind : Int) extends Exp
  case class Fun(body : Exp) extends Exp
  case class App(funExpr : Exp, argExpr : Exp) extends Exp

  type Env = List[Value]

  case class ClosureV(body : Exp, env : Env) extends Value

  def applyClosure(c: Value, arg: Value) : Value = c match {
    case ClosureV(body, closureEnv) => eval(body, arg :: closureEnv)
  }

  def eval(exp : Exp, env : Env) : Value = exp match {
    case Id(ind) => env(ind)
    case Fun(body) => ClosureV(body, env)
    case App(funExpr, argExpr) => applyClosure(eval(funExpr, env), eval(argExpr, env))
  }

  def interpret(exp : Exp) : Value = eval(exp, Nil)
}

Back to our original plan, since CPS-transformation reintroduced meta-level first-class functions and in turn higher-order functions as well into the closure-converted interpreter, we are going to apply defunctionalization to our CPS-transformed interpreter HOF_CC_CbNCPS.

There are two function types, namely Value => Value for continuation and Cont => Value for delayed computation. We will defunctionalize them separately. We choose to first handle Cont => Value. The other way around works as well and is left as an exercise.

Defunctionalizing Computation

For the function type Cont => Value, we introduce an encoding data type Computation. The is only one instance of type Cont => Value, that is, k => eval(argExpr, env, k).
It contains two free variables, namely argExpr of type Exp and env of type Env. Therefore we introduce one data constructor Thunk that takes one argument of type Exp and the other of type Env, without loss of generality, named argExpr and env respectively. The “apply” function, called applyComp is readily defined. During refactoring, the only function instance is replaced with Thunk(argExpr, env), and the application of the encoded function fun(k) is replaced with an explicit invocation of applyComp on env(ind) which is supposed to be data encoding compuation and the continuation k. The result is the following interpreter.

object HOF_CC_CbNCPS_DFC {
  sealed abstract class Exp
  sealed abstract class Value
  sealed abstract class Computation

  case class Id(ind: Int) extends Exp
  case class Fun(body: Exp) extends Exp
  case class App(funExpr: Exp, argExpr: Exp) extends Exp

  type Env = List[Computation]

  case class ClosureV(body: Exp, env: Env) extends Value

  type Cont = Value => Value

  case class Thunk(exp: Exp, env: Env) extends Computation

  def applyComp(thunk: Computation, k: Cont): Value = thunk match {
    case Thunk(exp, env) => eval(exp, env, k)
  }

  def eval(exp: Exp, env: Env, k: Cont): Value = exp match {
    case Id(ind) => applyComp(env(ind), k)
    case Fun(body) => k(ClosureV(body, env))
    case App(funExpr, argExpr) =>
      eval(funExpr, env, v => v match {
        case ClosureV(body, closureEnv) => 
          eval(body, Thunk(argExpr, env) :: closureEnv, k)
      })
  }

  def interpret(exp: Exp): Value = eval(exp, Nil, v => v)
}

Inlining

We can similarly inline applyComp, giving:

object HOF_CC_CbNCPS_DFC_Inl {
  sealed abstract class Exp
  sealed abstract class Value
  sealed abstract class Computation

  case class Id(ind: Int) extends Exp
  case class Fun(body: Exp) extends Exp
  case class App(funExpr: Exp, argExpr: Exp) extends Exp

  type Env = List[Computation]

  case class ClosureV(body: Exp, env: Env) extends Value

  type Cont = Value => Value

  case class Thunk(argExpr: Exp, env: Env) extends Computation

  def eval(exp: Exp, env: Env, k: Cont) : Value = exp match {
    case Id(ind) => env(ind) match {
      case Thunk(exp, env) => eval(exp, env, k)
    }
    case Fun(body) => k(ClosureV(body, env))
    case App(funExpr, argExpr) =>
      eval(funExpr, env, v => v match {
        case ClosureV(body, closureEnv) => eval(body, Thunk(argExpr, env) :: closureEnv, k)
      })
  }

  def interpret(exp: Exp): Value = eval(exp, Nil, v => v)
}

Defunctionalizing Continuation

Next we defunctionalize functions of type Value => Value for continuation. First, we introduce a new data type Cont. Second, we find that there are two instances of the function type: v => v match { … } and v => v. The former has three free variables: argExpr of type Exp, env of type Env and k of type Cont. The latter has no free variable. Therefore we introduce two data constructors: AppCont with two parameters exp of type Exp and env of type Env, IdCont with no parameter. The apply function, called applyCont is again easily defined. Third, we replace the two instances v => v match { … } and v => v with AppCont(exp, env, k) and IdCont() respectively, the application of them k(ClosureV(body, env) with an explicit invocation of applyCont on k and ClosureV(body, env). We obtain the following interpreter without meta-level first-class functions and higher-order functions.

object HOF_CC_CbNCPS_DFC_Inl_DFC {
  sealed abstract class Exp
  sealed abstract class Value
  sealed abstract class Computation
  sealed abstract class Cont

  case class Id(ind: Int) extends Exp
  case class Fun(body: Exp) extends Exp
  case class App(funExpr: Exp, argExpr: Exp) extends Exp

  type Env = List[Computation]

  case class ClosureV(body: Exp, env: Env) extends Value

  case class Thunk(exp: Exp, env: Env) extends Computation

  case class AppCont(exp: Exp, env: Env, k: Cont) extends Cont
  case class IdCont() extends Cont

  def applyCont(k: Cont, v: Value): Value = k match {
    case AppCont(exp, env, k) => v match {
      case ClosureV(body, closureEnv) => eval(body, Thunk(exp, env) :: closureEnv, k)
    }
    case IdCont() => v
  }

  def eval(exp: Exp, env: Env, k: Cont): Value = exp match {
    case Id(ind) => env(ind) match {
      case Thunk(exp, env) => eval(exp, env, k)
    }
    case Fun(body) => applyCont(k, ClosureV(body, env))
    case App(funExpr, argExpr) => eval(funExpr, env, AppCont(argExpr, env, k))
  }

  def interpret(exp: Exp): Value = eval(exp, Nil, IdCont())
}

Refactoring (Again)

Notice the structural similarity between the two constructors ClosureV and Thunk: both take as arguments an Exp and an Env. They can be unified them by keeping only one of them, say Thunk. Note that Computation is renamed ClosureV as a reminder for this unification.

Next observe another structual similarity between constructors of data type Cont and those of Scala’s built-in generic type List[T]: IdCont is isomorphic to Nil and AppCont is isomorphic to :: if we group Exp and Env together as one data type, naturally ClosureV. Thanks to this isomorphism, we can use Nil and :: in place of IdCont and AppCont. In other words, we can listify continuations.

Again we can inline applyCont.

Finally, we modify the return type of eval to be the triple (Exp, Env, Cont).
Now if we consider the triple as a kind of “state”, eval can be viewed as a state transition function:

(Exp, Env, Cont) ==eval=> (Exp, Env, Cont)

More accurately, eval takes a big-step, jumping from an initial state (exp, Nil, Nil) directly to a final state (body, env, Nil).

We will soon see that we thus obtain an implementaion of an abstract machine, called Krivine’s machine.

object KAM {
  sealed abstract class Exp
  sealed abstract class ClosureV

  case class Id(ind: Int) extends Exp
  case class Fun(body: Exp) extends Exp
  case class App(funExpr: Exp, argExpr: Exp) extends Exp

  type Env = List[ClosureV]

  case class Thunk(exp: Exp, env: Env) extends ClosureV

  type Cont = List[ClosureV]

  def eval(exp: Exp, env: Env, k: Cont): (Exp, Env, Cont) = exp match {
    case Id(ind) => env(ind) match {
      case Thunk(exp, env) => eval(exp, env, k)
    }
    case Fun(body) => k match {
      case Nil => (body, env, Nil)
      case (Thunk(exp, thunkEnv) :: k) => eval(body, Thunk(exp, thunkEnv) :: env, k)
    }
    case App(funExpr, argExpr) => eval(funExpr, env, Thunk(argExpr, env) :: k)
  }

  def interpret(exp: Exp): (Exp, Env, Cont) = eval(exp, Nil, Nil) 
}

Krivine’s Abstract Machine

An abstract machine is a conceptual model of a computer system. It seems a machine because it allows step-by-step program execution. It is abstract in that it leaves out lots of subtle details of real machines. A virtual machine (like the JVM) can be similarly charaterized except that it also has an instruction set. Source programs must first be compiled to object programs in these instructions before they can be executed by a virtual machine. An abstract machine does not have such an instruction set and in turn it executes source programs directly. In this sense, an abstract machine works like an interpreter. Like a real machine, an abstract machine also has states and works by updating states. In other words, one run of an abstract machine can be viewed as a series of state transition. To understand an abstract machine boils down to understand its states and transition function.

We present here an abstract machine, called Krivine’s machine (simply K-machine), for the call-by-name lambda-calculus. The K-machine has three registers: Exp to store lambda-expressions, Env to keep thunks, and Cont to save continuations. The triple (Exp, Env, Cont) completely renders the state of the K-machine. Before we see the transition function of the K-machine, we need to know the syntax for things stored in these three registers.

The syntax for lambda-expressions is:

(expression)  e ::= i    (index)
                  | \ e  (abstraction)
                  | e e  (application)

That is, a lambda-expression can be a natural number (as de-Bruijn index for a nameless variable), or “\” (mimicing the look of the Greek letter lambda) followed by an expression (together notating lambda-abstraction, designating anonymous function), or two expressions juxtaposed (together designating function application). Here are some lambda-expressions in this syntax (call it “de Bruijn”) and Scala syntax:

| de Bruijn | Scala                       |
| \ 0       | (x : Any) => x              |
| \ \ 1     | (x : Any) => (y : Any) => x |

Recall that de-Bruijn indices for a lambda-abstraction start with 0 from the innermost lambda-bound variable and count outward. Thus the inner parameter y in the example (x : Any) => (y : Any) => x is indexed 0, the outer x indexed 1. Hence the body of the de-Bruijn notation is 1. Note that Scala is typed whereas the pure lambda-calculus is not. That is why there is no type annotation in the corresponding lambda-expression in de-Bruijn syntax.

The syntax for environments is:

(environment) r ::= []
                  | (e, r) :: r

That is, an environment is either empty or a thunk bundled with other thunks accumulated into another environment.

The syntax for continuation is as follows:

(continuation) c ::= []
                   | (e, r) :: c

That is, a continuation is either nothing (left to do) or “to apply to a thunk” bundled with to-dos afterwards which manifests as another continuation.

Notice that even though environments and continuations have isomorphic syntactic structure, they have different interpretations as clearly stated above.

Now we are ready to see the state-transition function of the K-machine. It is given by three transition rules (numbered from 1 to 3) presented in the following table:

| (Exp , Env, Cont         ) | ——> | (Exp, Env          , Cont        ) |
| (i   , r  , c            ) | —1-> | (e1 , r1           , c           ) |
|                            |       |          where r(i) = (e1, r1)     |
| (\ e , r  , (e1, r1) :: c) | —2-> | (e  , (e1, r1) :: r, c           ) |
| (e e1, r  , c            ) | —3-> | (e  , r            , (e1, r) :: c) |

Each transition rule covers one-step transition of states. A run of the K-machine is a series of these transition steps. There must be an initial state and a final state. Let the lambda-program to be executed on the K-machine, in other words, the lambda-expression to be evaluated by the K-machine, be e. The initial state of a run will always be (e, [], []), that is, with the register Exp initialized to e, and the register Env to empty and the register Cont to nothing. The final states of runs for different expressions may be different. But two things are common: the machine halts when there is nothing left to do, that is, the register Cont must be []. So the final state will always have the pattern (e, r, []). e in the register Exp together with r in the register Env could be taken as the result. Let us take a simple example, (\ 0) (\ 0), and simulate its run on the K-machine.

      ( Exp        , Env            , Cont            )
      ( (\ 0) (\ 0), []             , []              )
—3-> ( \ 0        , []             , (\ 0, []) :: [] )
—2-> ( 0          , (\ 0, []) :: [], []              )
—1-> ( \ 0        , []             , []              )

To see another example, (\ \ 1) (\ 0), in run:

      ( Exp          , Env            , Cont            )
      ( (\ \ 1) (\ 0), []             , []              )
—3-> ( \ \ 1        , []             , (\ 0, []) :: [] )
—2-> ( \ 1          , (\ 0, []) :: [], []              )

This example shows that in the final state, the register Env does not necessarily hold the empty environment.

As an exercise, try to simulate the execution of ((\ \ 1) (\ 0)) (\ 0) on the K-machine.

We see that to evaluate a lambda-expression e, the K-machine sets the initial state to (e, [], []), applies the three transition rules repeatedly (for zero or more times) until the register Cont holds [] again, which means there is nothing left to do, and then halts. This hints the following idea for an implementation of the K-machine: we can define a Scala function, red1(exp : Exp, env : Env, k : Cont) : (Exp, Env, Cont), and then form a do-while loop with a loop variable s : (Exp, Env, Cont) initialized to (exp, [], []), with a loop condition checking whether k is [], and in the body tries to apply one of the transition rules. The actual implementation is left as an exercise.

We finally remark that KAM we have defined above is indeed such an implementation. Note that eval is tail-recursive, thus it is equivalent to a loop in effect. The difference between eval and red1 is: the former implements a big-step transition, while the latter a small-step transition.

To conclude, we have demonstrated how to reach an abstract-machine-level impelementation of the call-by-name lambda-calculus from a high-level implementation using higher-order features by performing a series of program transformations on the evaluators. Next, we will do reverse engineering: we will start with an abstract-machine-level implementation of the call-by-value lambda-calculus, do the reverse of those program transformations we have seen, and reach a high-level implementation using higher-order features.

From Abstract Machines to Evaluators

The CEK-Machine

object CEK {
  sealed abstract class Exp
  sealed abstract class Value
  sealed abstract class Ctx

  case class Id(ind : Int) extends Exp
  case class Fun(body : Exp) extends Exp
  case class App(funExpr : Exp, argExpr : Exp) extends Exp

  type Env = List[Value]

  case class ClosureV(body : Exp, env : Env) extends Value

  case class IdCont() extends Ctx
  case class ACL(argExpr : Exp, env : Env, ctx : Ctx) extends Ctx
  case class ACR(clo : Value, ctx : Ctx) extends Ctx

  def applyCont(ctx : Ctx, v : Value) : Value = ctx match {
    case IdCont() => v
    case ACL(argExpr, env, ctx) => v match {
      case clo@ClosureV(_, _) => eval(argExpr, env, ACR(clo, ctx))
    }
    case ACR(ClosureV(body, closureEnv), ctx) => eval(body, v :: closureEnv, ctx)
  }

  def eval(exp : Exp, env : Env, ctx : Ctx) : Value = exp match {
    case Id(ind) => applyCont(ctx, env(ind))
    case Fun(body) => applyCont(ctx, ClosureV(body, env))
    case App(funExpr, argExpr) => eval(funExpr, env, ACL(argExpr, env, ctx))
  }

  def interpret(exp : Exp) : Value = eval(exp, Nil, IdCont())
}

Refunctionalization

object CEK_RF {
  sealed abstract class Exp
  sealed abstract class Value

  case class Id(ind : Int) extends Exp
  case class Fun(body : Exp) extends Exp
  case class App(funExpr : Exp, argExpr : Exp) extends Exp

  type Env = List[Value]

  case class ClosureV(body : Exp, env : Env) extends Value

  type Ctx = Value => Value

  def eval(exp : Exp, env : Env, ctx : Ctx) : Value = exp match {
    case Id(ind) => ctx(env(ind))
    case Fun(body) => ctx(ClosureV(body, env))
    case App(funExpr, argExpr) =>
      eval( funExpr, env
          , clo => eval( argExpr, env
                       , arg => clo match {
                           case ClosureV(body, closureEnv) => eval(body, arg :: closureEnv, ctx)
                         } ) )
  }

  def interpret(exp : Exp) : Value = eval(exp, Nil, v => v)
}

Direct-Style Transformation

object CEK_RF_DS {
  sealed abstract class Exp
  sealed abstract class Value

  case class Id(ind : Int) extends Exp
  case class Fun(body : Exp) extends Exp
  case class App(funExpr : Exp, argExpr : Exp) extends Exp

  type Env = List[Value]

  case class ClosureV(body : Exp, env : Env) extends Value

  def eval(exp : Exp, env : Env) : Value = exp match {
    case Id(ind) => env(ind)
    case Fun(body) => ClosureV(body, env)
    case App(funExpr, argExpr) => eval(funExpr, env) match {
      case ClosureV(body, closureEnv) => eval(body, eval(argExpr, env) :: closureEnv)
    }
  }

  def interpret(exp : Exp, env : Env) : Value = eval(exp, Nil)
}

Higher-Order-Function Conversion

object CEK_RF_DS_HOF {
  sealed abstract class Exp
  sealed abstract class Value

  case class Id(ind : Int) extends Exp
  case class Fun(body : Exp) extends Exp
  case class App(funExpr : Exp, argExpr : Exp) extends Exp

  type Env = List[Value]

  case class Prc(fun : Value => Value) extends Value

  def eval(exp : Exp, env : Env) : Value = exp match {
    case Id(ind) => env(ind)
    case Fun(body) => Prc(arg => eval(body, arg :: env))
    case App(funExpr, argExpr) => eval(funExpr, env) match {
      case Prc(fun) => fun(eval(argExpr, env))
    }
  }

  def interpret(exp : Exp) : Value = eval(exp, Nil)
}

Rationalized *


object HOF { sealed abstract class Imp

case class Id(ind : Int) extends Imp case class Fun(body : Imp) extends Imp case class App(funExpr : Imp, argExpr : Imp) extends Imp case class Prc(fun : Imp => Imp) extends Imp

type Env = List[Imp]

def norm(imp : Imp, env : Env) : Imp = imp match { case Id(ind) => env(ind) case Fun(body) => Prc(arg => norm(body, arg :: env)) case App(funExpr, argExpr) => norm(funExpr, env) match { case Prc(fun) => fun(norm(argExpr, env)) } }

def interpret(imp : Imp) : Imp = norm(imp, Nil) }

/* ## Call-by-Name CPS-Transformation

object HOF_CbNCPS {
  sealed abstract class Imp

  case class Id(ind : Int) extends Imp
  case class Fun(body : Imp) extends Imp
  case class App(funExpr : Imp, argExpr : Imp) extends Imp

  type Env = List[Imp]

  type Cont = Imp => Imp

  case class Prc(fun : Imp => Cont => Imp) extends Imp
  case class Cmp(fun : Cont => Imp) extends Imp

  def norm(imp : Imp, env : Env, k : Cont) : Imp = imp match {
    case Id(ind) => env(ind) match {
      case Cmp(fun) => fun(k)
    }
    case Fun(body) => k(Prc(cmp => k => norm(body, cmp :: env, k)))
    case App(funExpr, argExpr) =>
      norm( funExpr, env
          , prc => prc match {
              case Prc(fun) => fun(Cmp(k => norm(argExpr, env, k)))(k)
            } )
  }

  def interpret(imp : Imp) : Imp = norm(imp, Nil, nfd => nfd)
}

Defunctionalizing Procedures and Computation

object HOF_CbNCPS_DFPC {
  sealed abstract class Imp

  case class Id(ind : Int) extends Imp
  case class Fun(body : Imp) extends Imp
  case class App(funExpr : Imp, argExpr : Imp) extends Imp

  type Env = List[Imp]

  case class ClosureV(body : Imp, env : Env) extends Imp
  case class Thunk(argExpr : Imp, env : Env) extends Imp

  type Cont = Imp => Imp

  def applyClosure(clo : Imp, cmp : Imp, k : Cont) : Imp = clo match {
    case ClosureV(body, closureEnv) => norm(body, cmp :: closureEnv, k)
  }

  def apTh(thk : Imp, k : Cont) : Imp = thk match {
    case Thunk(argExpr, thunkEnv) => norm(argExpr, thunkEnv, k)
  }

  def norm(imp : Imp, env : Env, k : Cont) : Imp = imp match {
    case Id(ind) => apTh(env(ind), k)
    case Fun(body) => k(ClosureV(body, env))
    case App(funExpr, argExpr) =>
      norm( funExpr, env
          , clo => applyClosure(clo, Thunk(argExpr, env), k) )
  }

  def interpret(imp : Imp) : Imp = norm(imp, Nil, nfd => nfd)
}

Defunctionalizing Continuation

object HOF_CbNCPS_DFPC_DFC {
  sealed abstract class Imp
  sealed abstract class Cont

  case class Id(ind : Int) extends Imp
  case class Fun(body : Imp) extends Imp
  case class App(funExpr : Imp, argExpr : Imp) extends Imp

  type Env = List[Imp]

  case class ClosureV(body : Imp, env : Env) extends Imp
  case class Thunk(argExpr : Imp, env : Env) extends Imp

  case class IdCont() extends Cont
  case class AppCont(argExpr : Imp, env : Env, k : Cont) extends Cont

  def applyClosure(clo : Imp, cmp : Imp, k : Cont) : Imp = clo match {
    case ClosureV(body, closureEnv) => norm(body, cmp :: closureEnv, k)
  }

  def apTh(thk : Imp, k : Cont) : Imp = thk match {
    case Thunk(argExpr, thunkEnv) => norm(argExpr, thunkEnv, k)
  }

  def applyCont(k : Cont, clo : Imp) : Imp = k match {
    case IdCont() => clo
    case AppCont(argExpr, contEnv, k) => applyClosure(clo, Thunk(argExpr, contEnv), k)
  }

  def norm(imp : Imp, env : Env, k : Cont) : Imp = imp match {
    case Id(ind) => apTh(env(ind), k)
    case Fun(body) => applyCont(k, ClosureV(body, env))
    case App(funExpr, argExpr) => norm(funExpr, env, AppCont(argExpr, env, k))
  }

  def interpret(imp : Imp) : Imp = norm(imp, Nil, IdCont())
}

Refactoring

object HOF_CbNCPS_DFP_DFC_Rfc {
  sealed abstract class Imp

  case class Id(ind : Int) extends Imp
  case class Fun(body : Imp) extends Imp
  case class App(funExpr : Imp, argExpr : Imp) extends Imp

  type Env = List[Imp]

  case class ClosureV(imp : Imp, env : Env) extends Imp

  type Cont = List[Imp]

  def norm(imp : Imp, env : Env, k : Cont) : Imp = imp match {
    case Id(ind) => env(ind) match {
      case ClosureV(argExpr, closureEnv) => norm(argExpr, closureEnv, k)
    }
    case Fun(body) => k match {
      case Nil => ClosureV(body, env)
      case ClosureV(argExpr, thunkEnv) :: k => norm(body, ClosureV(argExpr, thunkEnv) :: env, k)
    } 
    case App(funExpr, argExpr) => norm(funExpr, env, ClosureV(argExpr, env) :: k)
  }

  def interpret(imp : Imp) : Imp = norm(imp, Nil, Nil)
}

object HOF_CbVCPS {
  sealed abstract class Imp

  case class Id(ind : Int) extends Imp
  case class Fun(body : Imp) extends Imp
  case class App(funExpr : Imp, argExpr : Imp) extends Imp

  type Env = List[Imp]

  type Cont = Imp => Imp

  case class Prc(fun : Imp => Cont => Imp) extends Imp

  def norm(imp : Imp, env : Env, k : Cont) : Imp = imp match {
    case Id(ind) => k(env(ind))
    case Fun(body) => k(Prc(arg => k => norm(body, arg :: env, k)))
    case App(funExpr, argExpr) =>
      norm( funExpr, env
          , prc => norm( argExpr, env
                       , arg => prc match {
                            case Prc(fun) => fun(arg)(k)
                         } ) )
  }

  def interpret(imp : Imp) : Imp = norm(imp, Nil, nfd => nfd)
}

object HOF_CbVCPS_DFP {
  sealed abstract class Imp

  case class Id(ind : Int) extends Imp
  case class Fun(body : Imp) extends Imp
  case class App(funExpr : Imp, argExpr : Imp) extends Imp

  type Env = List[Imp]

  case class ClosureV(body : Imp, env : Env) extends Imp

  type Cont = Imp => Imp

  def applyClosure(prc : Imp, arg : Imp, k : Cont) : Imp = prc match {
    case ClosureV(body, closureEnv) => norm(body, arg :: closureEnv, k)
  }

  def norm(imp : Imp, env : Env, k : Cont) : Imp = imp match {
    case Id(ind) => k(env(ind))
    case Fun(body) => k(ClosureV(body, env))
    case App(funExpr, argExpr) =>
      norm( funExpr, env
          , clo => norm( argExpr, env
                       , arg => applyClosure(clo, arg, k) ) )
  }
}

object HOF_CbVCPS_DFP_DFC {
  sealed abstract class Imp
  sealed abstract class Cont

  case class Id(ind : Int) extends Imp
  case class Fun(body : Imp) extends Imp
  case class App(funExpr : Imp, argExpr : Imp) extends Imp

  type Env = List[Imp]

  case class ClosureV(body : Imp, env : Env) extends Imp

  case class IdCont() extends Cont
  case class ACL(argExpr : Imp, env : Env, k : Cont) extends Cont
  case class ACR(clo : Imp, k : Cont) extends Cont

  def applyClosure(prc : Imp, arg : Imp, k : Cont) : Imp = prc match {
    case ClosureV(body, closureEnv) => norm(body, arg :: closureEnv, k)
  }

  def norm(imp : Imp, env : Env, k : Cont) : Imp = imp match {
    case Id(ind) => k(env(ind))
    case Fun(body) => k(ClosureV(body, env))
    case App(opr, argExpr) =>
      norm( opr, env
          , clo => norm( argExpr, env
                       , arg => applyClosure(clo, arg, k) ) )
  }
}

 ****************
 * Rationalized *
 ****************/