Lecture 8 — 2015-09-28

Call-by-value, call-by-name; interpreters

This lecture is written in literate Haskell; you can download the raw source.

Small step semantics for the lambda calculus

We talked about different stragegies for applying the equational semantics of the lambda calculus. We settled on two rewrite semantics for the lambda calculus: call-by-name, which is more like Haskell, and call-by-value, which is more like conventional languages.

The call-by-name semantics never bothers to evaluate its arguments. It just evalutes the head of each application, and then substitutes whatever arguments are there wholesale.

e1 --> e1'
---------------- CBNLeft
e1 e2 --> e1' e2

------------------------ CBNBeta
(\x. e1) e2 --> e1[e2/x]

The call-by-value semantics forces functions to evaluate their arguments, evaluating both sides of an application completely before doing beta reduction.

e1 --> e1'
---------------- CBVLeft
e1 e2 --> e1' e2

e2 --> e2'
---------------- CBVRight
v1 e2 --> v1 e2'

------------------------ CBVBeta
(\x. e1) v2 --> e1[v2/x]

Big step semantics

As we moved towards implementing our code, I said that substitution isn’t a great strategy. Instead, we’ll use environments.

We defined a big step semantics in math, which completely evaluates its inputs. The key new development here is closures, the combination of code—a lambda—and its environment.

Environments are maps from variable names to values, and values are closures. Here’s the full definition:

e in Term ::= x | \x. e | e1 e2
env in Env = Var -> Value
v in Value ::= <\x. e, env>

We defined a big-step rewrite system that evaluated everything in one go. I used something like ⇓. That’s hard to typeset here, so I’ll use VV. Here VV2Term × Env × Value.

Each rule takes a term and its current environment to a value. The rule for variables is easy: just look up the variable in the environment!

env(x) = v
----------- Var 
x, env VV v 

The rule for lambdas is also easy: they just become closures, holding onto the environment in which they were defined.

-------------------------- Lam
\x. e, env VV <\x. e, env>

The interesting rule is for applications. We evaluate each of the subterms in the current environment, env. Whatever term is in the function position—e1—must evaluate to a closure, <\x. e, env'>. We then run the body of the closure—e—in the closure’s environment extended with v2 bound for x. (We write env[x |-> v2] to talk about extending the environment.)

e1, env VV <\x. e, env'>    e2, env VV v2
e,env'[x |-> v2] VV v
----------------------------------------- App
e1 e2, env VV v

Moving to code

Substitution is annoying to implement. Let’s use a different strategy: environments.

import qualified Data.Map as Map
import Data.Map (Map, (!))

The pure lambda calculus has three forms: variables, applications, and lambda abstractions.

type Id = String

data LCExpr = 
    LCVar Id
  | LCApp LCExpr LCExpr
  | LCLam Id LCExpr
  deriving (Show,Eq)

We define values and environment mutually recursively: the only kind of values are closures (which have environments); environments are maps from variable names to values (which must be closures, which have environments, etc.).

data LCValue = 
  Closure Id LCExpr Env deriving Eq
type Env = Map Id LCValue

instance Show LCValue where
  show (Closure x e _) = "\\" ++ x ++ ". " ++ show e

extend :: Env -> Id -> LCValue -> Env
extend env x v = Map.insert x v env

Can you relate each part of this definition to something in the math?

evalLC :: Env -> LCExpr -> LCValue
evalLC env (LCVar x) = env ! x
evalLC env (LCLam x e) = Closure x e env
evalLC env (LCApp e1 e2) = 
  case evalLC env e1 of
    Closure x e env' -> evalLC (extend env' x (evalLC env e2)) e