Call-by-value is the most commonly used evaluation strategy in which all arguments to a function are reduced to normal form before they are bound inside lambda. Languages such as Java, C++, Scala and F# all use this evaluation model. A notable exception is Haskell, which uses call-by-need evaluation in which expressions are represented as thunks which are passed into a function unevaluated and only evaluated when needed.
This difference in evaluation model poses some challenges in writing a call-by-value interpreter in Haskell. In this post, I am going to explain how we can implement a call-by-value interpreter using various methods.
Let’s start the discussion by writing a lambda calculus interpreter.
data Expr
= Var Int
| Lam Expr
| App Expr Expr
| Lit Int
| Prim PrimOp Expr Expr
| Bot
deriving Show
data Value
= VInt Int
| VClosure Expr Env
instance Show Value where
show (VInt i) = show i
show VClosure{} = "<<closure>>"
data PrimOp = Add | Mul
deriving Show
type Env = [Value]
eval :: Env -> Expr -> Value
eval env term = case term of
Var n -> env !! n
Lam a -> VClosure a env
App a b ->
let VClosure c env' = eval env a in
let v = eval env b in
eval (v : env') c
Lit n -> VInt n
Prim p a b -> (evalPrim p) (eval env a) (eval env b)
Bot -> error "Evaluation would not terminate"
evalPrim :: PrimOp -> Value -> Value -> Value
evalPrim Add (VInt a) (VInt b) = VInt (a + b)
evalPrim Mul (VInt a) (VInt b) = VInt (a * b)
emptyEnv :: Env
emptyEnv = []
-- (\x y -> x) 10 bot
test :: Value
test = eval emptyEnv $ App (App (Lam (Lam (Var 1))) (Lit 10)) Bot
Can you guess the evaluation order implemented by this interpreter? Because test
is equivalent to (\x y -> x) 10 undefined
, it would be undefined
in a call-by-value language.
Let’s evaluate test
on GHCi.
λ> test
10
The evaluation order implemented by our interpreter is call-by-need because the defining language, Haskell, uses the call-by-need evaluation order and our interpreter depends on this. Transforming our interpreter into a call-by-value interpreter is not trivial because we need to find and fix every place where lazy evaluation is used in our interpreter.
In his seminar paper Definitional interpreters for higher-order programming languages, John C. Reynolds showed how to remove this order dependence by CPS transformation. But in Haskell, we can use monads to enforce the evaluation order. This is not a coincidence because there is a close relationship between computational monads and generalized CPS.
UPDATE: There is a technical mistake in the original article. The Identity monad does not make any difference here. I should have used either a strict variant of Identity monad or the Cont monad to force strict evaluation.
Here’s a monadic version of our interpreter.
import Control.Monad.Identity
data Expr
= Var Int
| Lam Expr
| App Expr Expr
| Lit Int
| Prim PrimOp Expr Expr
| Bot
deriving Show
data Value
= VInt Int
| VClosure Expr Env
instance Show Value where
show (VInt i) = show i
show VClosure{} = "<<closure>>"
data PrimOp = Add | Mul
deriving Show
type Env = [Value]
eval :: (Monad m) => Env -> Expr -> m Value
eval env term = case term of
Var n -> return $ env !! n
Lam a -> return $ VClosure a env
App a b -> do
VClosure c env' <- eval env a
v <- eval env b
eval (v : env') c
Lit n -> return $ VInt n
Prim p a b -> evalPrim p <$> eval env a <*> eval env b
Bot -> error "Evaluation would not terminate"
evalPrim :: PrimOp -> Value -> Value -> Value
evalPrim Add (VInt a) (VInt b) = VInt (a + b)
evalPrim Mul (VInt a) (VInt b) = VInt (a * b)
emptyEnv :: Env
emptyEnv = []
-- (\x y -> x) 10 bot
test :: Value
test = runIdentity $ eval emptyEnv $ App (App (Lam (Lam (Var 1))) (Lit 10)) Bot
Let’s evaluate test
again.
λ> test
10
Oops. What went wrong? The problem is that our interpreter does not enforce the evaluation of the argument in App a b
case of eval
. v <- eval env b
just binds a thunk to v
and it won’t be evaluated until it is actually needed. To fix the problem, we need to force the evaluation of the argument using bang patterns.
UPDATE: This bang pattern is not necessary if we used a strict monad.
{-# LANGUAGE BangPatterns #-}
...
eval :: (Monad m) => Env -> Expr -> m Value
eval env term = case term of
Var n -> return $ env !! n
Lam a -> return $ VClosure a env
App a b -> do
VClosure c env' <- eval env a
!v <- eval env b
eval (v : env') c
Lit n -> return $ VInt n
Prim p a b -> evalPrim p <$> eval env a <*> eval env b
Bot -> error "Evaluation would not terminate"
...
Finally, we can see that evaluating test
throws an error.
λ> test
*** Exception: Evaluation would not terminate
The moral of this story is that it is really hard to correctly implement a call-by-value interpreter in Haskell. There is high chance of making a mistake. For example, let’s add a division operator to our interpreter.
data PrimOp = Add | Mul | Div
deriving Show
evalPrim :: PrimOp -> Value -> Value -> Value
evalPrim Add (VInt a) (VInt b) = VInt (a + b)
evalPrim Mul (VInt a) (VInt b) = VInt (a * b)
evalPrim Div (VInt a) (VInt b) = VInt (a `div` b)
-- (\x y -> x) 10 (20 / 0)
test :: Value
test = runIdentity $ eval emptyEnv $ App (App (Lam (Lam (Var 1))) (Lit 10)) (Prim Div (Lit 20) (Lit 0))
Evaluating test
must throw an divide-by-zero error because its second argument is 20 / 0
. But GHCi shows that we reverted back to cal-by-need.
λ> test
10
This happens because the data constructor VInt
is not strict. 20 / 0
is evaluated to VInt undefined
instead of undefined
. To make it call-by-value again, we need to add another bang pattern to VInt
data constructor as follows:
data Value
= VInt !Int
| VClosure Expr Env
Fortunately, we can avoid this tricky business and make our first interpreter call-by-value by just adding Strict language extension introduced in GHC 8. Strict
pragma allows us to switch the default evaluation strategy to call-by-value on a per module basis. This saves us huge efforts because writing a call-by-value interpreter in a call-by-value language is an easy task!
{-# LANGUAGE Strict #-}