mirror of
https://github.com/sdiehl/write-you-a-haskell.git
synced 2024-08-16 15:11:06 +03:00
560 lines
15 KiB
Markdown
560 lines
15 KiB
Markdown
<div class="pagetitle">
|
|
![](img/titles/evaluation.png)
|
|
</div>
|
|
|
|
> *Well-typed programs cannot "go wrong".*
|
|
>
|
|
> <cite>— Robin Milner</cite>
|
|
|
|
<p class="halfbreak">
|
|
</p>
|
|
|
|
Evaluation
|
|
==========
|
|
|
|
While the lambda calculus is exceedingly simple, there is a great deal of
|
|
variety in ways to evaluate and implement the reduction of lambda expressions.
|
|
The different models for evaluation are *evaluation strategies*.
|
|
|
|
There is a bifurcation between two points in the design space: *strict* and
|
|
*non-strict* evaluation. An evaluation strategy is strict if the arguments to a
|
|
lambda expression are necessarily evaluated before a lambda is reduced. A
|
|
language in which the arguments are not necessarily evaluated before a lambda is
|
|
reduced is non-strict.
|
|
|
|
Alternatively expressed, diverging terms are represented up to equivalence by
|
|
the *bottom* value, written as $\bot$. A function $f$ is non-strict if:
|
|
|
|
$$
|
|
f \bot \neq \bot
|
|
$$
|
|
|
|
Evaluation Models
|
|
-----------------
|
|
|
|
There are many different models, and various hybrids thereof. We will consider three
|
|
dominant models:
|
|
|
|
* Call-by-value: arguments are evaluated before a function is entered
|
|
* Call-by-name: arguments are passed unevaluated
|
|
* Call-by-need: arguments are passed unevaluated but an expression is only evaluated
|
|
once and shared upon subsequent references
|
|
|
|
Given an expression $f x$ the reduction in different evaluation models proceeds
|
|
differently:
|
|
|
|
*Call-by-value*:
|
|
|
|
1. Evaluate $x$ to $v$
|
|
2. Evaluate $f$ to $\lambda y. e$
|
|
3. Evaluate $[y/v]e$
|
|
|
|
*Call-by-name*:
|
|
|
|
1. Evaluate $f$ to $\lambda y. e$
|
|
2. Evaluate $[y/x]e$
|
|
|
|
*Call-by-need*:
|
|
|
|
1. Allocate a thunk $v$ for $x$
|
|
2. Evaluate $f$ to $\lambda y. e$
|
|
3. Evaluate $[y/v]e$
|
|
|
|
Terms that have a normal form in one model, may or may not have a normal form in
|
|
another. In call-by-need and call-by-name evaluation diverging terms are not
|
|
necessarily evaluated before entry, so some terms that have a normal form in
|
|
these models may diverge under call-by-value.
|
|
|
|
Call-by-value
|
|
-------------
|
|
|
|
Call by value is an extremely common evaluation model. Many programming
|
|
languages both imperative and functional use this evaluation strategy. The
|
|
essence of call-by-value is that there are two categories of expressions: *terms*
|
|
and *values*. Values are lambda expressions and other terms which are in normal
|
|
form and cannot be reduced further. All arguments to a function will be reduced to
|
|
normal form *before* they are bound inside the lambda and reduction only
|
|
proceeds once the arguments are reduced.
|
|
|
|
For a simple arithmetic expression, the reduction proceeds as follows. Notice
|
|
how the subexpression ``(2 + 2)`` is evaluated to normal form before being
|
|
bound.
|
|
|
|
```haskell
|
|
(\x. \y. y x) (2 + 2) (\x. x + 1)
|
|
=> (\x. \y. y x) 4 (\x. x + 1)
|
|
=> (\y. y 4) (\x. x + 1)
|
|
=> (\x. x + 1) 4
|
|
=> 4 + 1
|
|
=> 5
|
|
```
|
|
|
|
Naturally there are two evaluation rules for applications.
|
|
|
|
$$
|
|
\begin{array}{cl}
|
|
\displaystyle \frac{e_1 \to e_1'}{e_1 e_2 \to e_1' e_2} & \trule{E-App1} \\ \\
|
|
\displaystyle \frac{e_2 \to e_2'}{v_1 e_2 \to v_1 e_2'} & \trule{E-App2} \\ \\
|
|
\displaystyle {(\lambda x . e) v \to [x / v] e } & \trule{E-AppLam} \\ \\
|
|
\end{array}
|
|
$$
|
|
|
|
For a simple little lambda calculus the call-by-value interpreter is quite
|
|
simple. Part of the runtime evaluation of lambda calculus involves the creation
|
|
of *closures*, environments which hold the local variables in scope. In our
|
|
little language there are two possible values which reduction may converge on,
|
|
**VInt** and **VClosure**.
|
|
|
|
```haskell
|
|
data Expr
|
|
= Var Int
|
|
| Lam Expr
|
|
| App Expr Expr
|
|
| Lit Int
|
|
| Prim PrimOp Expr Expr
|
|
deriving Show
|
|
|
|
data PrimOp = Add | Mul
|
|
deriving Show
|
|
|
|
data Value
|
|
= VInt Int
|
|
| VClosure Expr Env
|
|
deriving Show
|
|
|
|
type Env = [Value]
|
|
|
|
emptyEnv :: Env
|
|
emptyEnv = []
|
|
```
|
|
|
|
The evaluator function simply maps the local scope and a term to the final
|
|
value. Whenever a variable is referred to it is looked up in the environment.
|
|
Whenever a lambda is entered it extends the environment with the local scope of
|
|
the closure.
|
|
|
|
```haskell
|
|
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)
|
|
|
|
evalPrim :: PrimOp -> Value -> Value -> Value
|
|
evalPrim Add (VInt a) (VInt b) = VInt (a + b)
|
|
evalPrim Mul (VInt a) (VInt b) = VInt (a * b)
|
|
```
|
|
|
|
Call-by-name
|
|
------------
|
|
|
|
In call-by-name evaluation, the arguments to lambda expressions are substituted
|
|
as is, evaluation simply proceeds from left to right substituting the outermost
|
|
lambda or reducing a value. If a substituted expression is not used it is never
|
|
evaluated.
|
|
|
|
$$
|
|
\begin{array}{cl}
|
|
\displaystyle \frac{e_1 \to e_1'}{e_1 e_2 \to e_1' e_2} & \trule{E-App} \\ \\
|
|
\displaystyle {(\lambda x . e_1) e_2 \to [x / e_2] e_1 } & \trule{E-AppLam} \\ \\
|
|
\end{array}
|
|
$$
|
|
|
|
For example, the same expression we looked at for call-by-value has the same
|
|
normal form but arrives at it by a different sequence of reductions:
|
|
|
|
```haskell
|
|
(\x. \y. y x) (2 + 2) (\x. x + 1)
|
|
=> (\y. y (2 + 2)) (\x. x + 1)
|
|
=> (\x. x + 1) (2 + 2)
|
|
=> (2 + 2) + 1
|
|
=> 4 + 1
|
|
=> 5
|
|
```
|
|
|
|
Call-by-name is non-strict, although very few languages use this model.
|
|
|
|
Call-by-need
|
|
------------
|
|
|
|
*Call-by-need* is a special type of non-strict evaluation in which unevaluated
|
|
expressions are represented by suspensions or *thunks* which are passed into a
|
|
function unevaluated and only evaluated when needed or *forced*. When the thunk
|
|
is forced the representation of the thunk is *updated* with the computed value
|
|
and is not recomputed upon further reference.
|
|
|
|
The thunks for unevaluated lambda expressions are allocated when evaluated, and
|
|
the resulting computed value is placed in the same reference so that
|
|
subsequent computations share the result. If the argument is never needed it is
|
|
never computed, which results in a trade-off between space and time.
|
|
|
|
<!--
|
|
Evaluation for call-by-need never has worse asymptotic time complexity than
|
|
call-by-value, but can result in worse space complexity.
|
|
-->
|
|
|
|
Since the evaluation of subexpression does not follow any pre-defined order, any
|
|
impure functions with side-effects will be evaluated in an unspecified order. As
|
|
a result call-by-need can only effectively be implemented in a purely functional
|
|
setting.
|
|
|
|
```haskell
|
|
type Thunk = () -> IO Value
|
|
|
|
data Value
|
|
= VBool Bool
|
|
| VInt Integer
|
|
| VClosure (Thunk -> IO Value)
|
|
```
|
|
|
|
```haskell
|
|
update :: IORef Thunk -> Value -> IO ()
|
|
update ref v = do
|
|
writeIORef ref (\() -> return v)
|
|
return ()
|
|
```
|
|
|
|
```haskell
|
|
force :: IORef Thunk -> IO Value
|
|
force ref = do
|
|
th <- readIORef ref
|
|
v <- th ()
|
|
update ref v
|
|
return v
|
|
```
|
|
|
|
```haskell
|
|
mkThunk :: Env -> String -> Expr -> (Thunk -> IO Value)
|
|
mkThunk env x body = \a -> do
|
|
a' <- newIORef a
|
|
eval ((x, a') : env) body
|
|
```
|
|
|
|
```haskell
|
|
eval :: Env -> Expr -> IO Value
|
|
eval env ex = case ex of
|
|
EVar n -> do
|
|
th <- lookupEnv env n
|
|
v <- force th
|
|
return v
|
|
|
|
ELam x e -> return $ VClosure (mkThunk env x e)
|
|
|
|
EApp a b -> do
|
|
VClosure c <- eval env a
|
|
c (\() -> eval env b)
|
|
|
|
EBool b -> return $ VBool b
|
|
EInt n -> return $ VInt n
|
|
EFix e -> eval env (EApp e (EFix e))
|
|
```
|
|
|
|
For example, in this model the following program will not diverge since the
|
|
omega combinator passed into the constant function is not used and therefore the
|
|
argument is not evaluated.
|
|
|
|
```haskell
|
|
omega = (\x -> x x) (\x -> x x)
|
|
test1 = (\y -> 42) omega
|
|
```
|
|
|
|
```haskell
|
|
omega :: Expr
|
|
omega = EApp (ELam "x" (EApp (EVar "x") (EVar "x")))
|
|
(ELam "x" (EApp (EVar "x") (EVar "x")))
|
|
|
|
test1 :: IO Value
|
|
test1 = eval [] $ EApp (ELam "y" (EInt 42)) omega
|
|
```
|
|
|
|
Higher Order Abstract Syntax (HOAS)
|
|
-----------------------------------
|
|
|
|
GHC Haskell being a rich language has a variety of extensions that, among other
|
|
things, allow us to map lambda expressions in our defined language directly onto
|
|
lambda expressions in Haskell. In this case we will use a GADT to embed a
|
|
Haskell expression inside our expression type.
|
|
|
|
```haskell
|
|
{-# LANGUAGE GADTs #-}
|
|
|
|
data Expr a where
|
|
Lift :: a -> Expr a
|
|
Tup :: Expr a -> Expr b -> Expr (a, b)
|
|
Lam :: (Expr a -> Expr b) -> Expr (a -> b)
|
|
App :: Expr (a -> b) -> Expr a -> Expr b
|
|
Fix :: Expr (a -> a) -> Expr a
|
|
```
|
|
|
|
The most notable feature of this encoding is that there is no distinct
|
|
constructor for variables. Instead they are simply values in the host
|
|
language. Some example expressions:
|
|
|
|
```haskell
|
|
id :: Expr (a -> a)
|
|
id = Lam (\x -> x)
|
|
|
|
tr :: Expr (a -> b -> a)
|
|
tr = Lam (\x -> (Lam (\y -> x)))
|
|
|
|
fl :: Expr (a -> b -> b)
|
|
fl = Lam (\x -> (Lam (\y -> y)))
|
|
```
|
|
|
|
Our evaluator then simply uses Haskell for evaluation.
|
|
|
|
```haskell
|
|
eval :: Expr a -> a
|
|
eval (Lift v) = v
|
|
eval (Tup e1 e2) = (eval e1, eval e2)
|
|
eval (Lam f) = \x -> eval (f (Lift x))
|
|
eval (App e1 e2) = (eval e1) (eval e2)
|
|
eval (Fix f) = (eval f) (eval (Fix f))
|
|
```
|
|
|
|
Some examples of use:
|
|
|
|
```haskell
|
|
fact :: Expr (Integer -> Integer)
|
|
fact =
|
|
Fix (
|
|
Lam (\f ->
|
|
Lam (\y ->
|
|
Lift (
|
|
if eval y == 0
|
|
then 1
|
|
else eval y * (eval f) (eval y - 1)))))
|
|
|
|
test :: Integer
|
|
test = eval fact 10
|
|
|
|
main :: IO ()
|
|
main = print test
|
|
```
|
|
|
|
Several caveats must be taken when working with HOAS. First of all, it takes more
|
|
work to transform expressions in this form since in order to work with the
|
|
expression we would need to reach under the lambda binder of a Haskell function
|
|
itself. Since all the machinery is wrapped up inside of Haskell's implementation
|
|
even simple operations like pretty printing and writing transformation passes
|
|
can be more difficult. This form is a good form for evaluation, but not for
|
|
transformation.
|
|
|
|
Parametric Higher Order Abstract Syntax (PHOAS)
|
|
-----------------------------------------------
|
|
|
|
A slightly different form of HOAS called PHOAS uses a lambda representation
|
|
parameterized over the binder type under an existential type.
|
|
|
|
```haskell
|
|
{-# LANGUAGE RankNTypes #-}
|
|
|
|
data ExprP a
|
|
= VarP a
|
|
| AppP (ExprP a) (ExprP a)
|
|
| LamP (a -> ExprP a)
|
|
| LitP Integer
|
|
|
|
newtype Expr = Expr { unExpr :: forall a . ExprP a }
|
|
```
|
|
|
|
The lambda in our language is simply a lambda within Haskell. As an example,
|
|
the usual SK combinators would be written as follows:
|
|
|
|
```haskell
|
|
-- i x = x
|
|
i :: ExprP a
|
|
i = LamP (\a -> VarP a)
|
|
|
|
-- k x y = x
|
|
k :: ExprP a
|
|
k = LamP (\x -> LamP (\y -> VarP x))
|
|
|
|
-- s f g x = f x (g x)
|
|
s :: ExprP a
|
|
s =
|
|
LamP (\f ->
|
|
LamP (\g ->
|
|
LamP (\x ->
|
|
AppP
|
|
(AppP (VarP f) (VarP x))
|
|
(AppP (VarP g) (VarP x))
|
|
)))
|
|
```
|
|
|
|
Evaluation will result in a runtime ``Value`` type, just as before with our
|
|
outer interpreters. We will use several "extractor" functions which use
|
|
incomplete patterns under the hood. The model itself does not prevent malformed
|
|
programs from blowing up here, and so it is necessary to guarantee that the
|
|
program is sound before evaluation. Normally this would be guaranteed at a
|
|
higher level by a typechecker before even reaching this point.
|
|
|
|
```haskell
|
|
data Value
|
|
= VLit Integer
|
|
| VFun (Value -> Value)
|
|
|
|
fromVFun :: Value -> (Value -> Value)
|
|
fromVFun val = case val of
|
|
VFun f -> f
|
|
_ -> error "not a function"
|
|
|
|
fromVLit :: Value -> Integer
|
|
fromVLit val = case val of
|
|
VLit n -> n
|
|
_ -> error "not an integer"
|
|
```
|
|
|
|
Evaluation simply exploits the fact that nestled up under our existential type
|
|
is just a Haskell function and so we get all the name capture, closures and
|
|
binding machinery for free. The evaluation logic for PHOAS model is extremely
|
|
short.
|
|
|
|
```haskell
|
|
eval :: Expr -> Value
|
|
eval e = ev (unExpr e) where
|
|
ev (LamP f) = VFun(ev . f)
|
|
ev (VarP v) = v
|
|
ev (AppP e1 e2) = fromVFun (ev e1) (ev e2)
|
|
ev (LitP n) = VLit n
|
|
```
|
|
|
|
Consider the ``S K K = I`` example again and check the result:
|
|
|
|
```haskell
|
|
skk :: ExprP a
|
|
skk = AppP (AppP s k) k
|
|
|
|
example :: Integer
|
|
example = fromVLit $ eval $ Expr (AppP skk (LitP 3))
|
|
```
|
|
|
|
We will use this evaluation technique extensively in writing interpreters for
|
|
our larger languages. It is an extremely convenient and useful method for
|
|
writing interpreters in Haskell.
|
|
|
|
Embedding IO
|
|
------------
|
|
|
|
As mentioned before, effects are first class values in Haskell.
|
|
|
|
In Haskell we don't read from a file directly, but create a value that represents
|
|
reading from a file. This allows us to very cleanly model an interpreter for
|
|
our language inside of Haskell by establishing a mapping between the base
|
|
operations of our language and existing function implementations of the
|
|
standard operations in Haskell, and using monadic operations to build up a
|
|
pure effectful computation as a result of interpretation. After
|
|
evaluation, we finally lift the resulting IO value into Haskell and execute the
|
|
results. This fits in nicely with the PHOAS model and allows us to
|
|
efficiently implement a fully-fledged interpreter for our language with
|
|
remarkably little code, simply by exploiting Haskell's implementation.
|
|
|
|
To embed IO actions inside of our interpreter we create a distinct ``VEffect``
|
|
value that will build up a sequenced IO computation during evaluation. This
|
|
value will be passed off to Haskell and reified into real world effects.
|
|
|
|
```haskell
|
|
data ExprP a
|
|
= VarP a
|
|
| GlobalP Name
|
|
| AppP (ExprP a) (ExprP a)
|
|
| LamP (a -> ExprP a)
|
|
| LitP Char
|
|
| EffectP a
|
|
|
|
data Value
|
|
= VChar Char
|
|
| VFun (Value -> Value)
|
|
| VEffect (IO Value)
|
|
| VUnit
|
|
|
|
fromVEff :: Value -> (IO Value)
|
|
fromVEff val = case val of
|
|
VEffect f -> f
|
|
_ -> error "not an effect"
|
|
```
|
|
|
|
```haskell
|
|
eval :: Expr -> Value
|
|
eval e = ev (unExpr e) where
|
|
ev (LamP f) = VFun(ev . f)
|
|
ev (AppP e1 e2) = fromVFun (ev e1) (ev e2)
|
|
ev (LitP n) = VChar n
|
|
ev (EffectP v) = v
|
|
ev (VarP v) = v
|
|
ev (GlobalP op) = prim op
|
|
|
|
-- Lift an effect from our language into Haskell IO.
|
|
run :: Expr -> IO ()
|
|
run f = void (fromVEff (eval f))
|
|
```
|
|
|
|
The ``prim`` function will simply perform a lookup on the set of builtin
|
|
operations, which we'll define with a bit of syntactic sugar for wrapping up
|
|
Haskell functions.
|
|
|
|
```haskell
|
|
unary :: (Value -> Value) -> Value
|
|
unary f = lam $ \a -> f a
|
|
|
|
binary :: (Value -> Value -> Value) -> Value
|
|
binary f = lam $ \a ->
|
|
lam $ \b -> f a b
|
|
|
|
prim :: Name -> Value
|
|
prim op = case op of
|
|
"putChar#" -> unary $ \x ->
|
|
VEffect $ do
|
|
putChar (fromVChar x)
|
|
return VUnit
|
|
|
|
"getChar#" -> VEffect $ do
|
|
val <- getChar
|
|
return (VChar val)
|
|
|
|
"bindIO#" -> binary $ \x y -> bindIO x y
|
|
"returnIO#" -> unary $ \x -> returnIO x
|
|
"thenIO#" -> binary $ \x y -> thenIO x y
|
|
```
|
|
|
|
For example ``thenIO#`` sequences effects in our language will simply squash two
|
|
``VEffect`` objects into one composite effect building up a new ``VEffect``
|
|
value that is using Haskell's monadic sequencing on the internal ``IO`` value.
|
|
|
|
```haskell
|
|
bindIO :: Value -> Value -> Value
|
|
bindIO (VEffect f) (VFun g) = VEffect (f >>= fromVEff . g)
|
|
|
|
thenIO :: Value -> Value -> Value
|
|
thenIO (VEffect f) (VEffect g) = VEffect (f >> g)
|
|
|
|
returnIO :: Value -> Value
|
|
returnIO a = VEffect $ return a
|
|
```
|
|
|
|
Effectively we're just recreating the same conceptual relationship that Haskell
|
|
IO has with its runtime, but instead our host language uses Haskell as the
|
|
runtime!
|
|
|
|
Full Source
|
|
-----------
|
|
|
|
**Evaluation**
|
|
|
|
* [Call-by-value](https://github.com/sdiehl/write-you-a-haskell/blob/master/chapter6/interp.hs)
|
|
* [Call-by-need](https://github.com/sdiehl/write-you-a-haskell/blob/master/chapter6/lazy.hs)
|
|
|
|
**Higher Order Interpreters**
|
|
|
|
* [HOAS](https://github.com/sdiehl/write-you-a-haskell/blob/master/chapter6/hoas.hs)
|
|
* [PHOAS](https://github.com/sdiehl/write-you-a-haskell/blob/master/chapter6/phoas.hs)
|
|
* [Embedding IO](https://github.com/sdiehl/write-you-a-haskell/blob/master/chapter6/io.hs)
|
|
|
|
\pagebreak
|