Table of Contents

Lazy Evaluation in Haskell

Introduction

Lazy evaluation means that:

  1. an expression (function application) will be evaluated only when it is needed (precisely as in the Lambda Calculus's normal evaluation)
  2. an expression is evaluated only once

We illustrate point 1. via the following example:

nats = 0:(map (+1) nats)
test = foldr (\x y-> if x > 2 then 0 else x+y) 10 nats
0 op (1 op (2 op (3 op (4 op .... 
0 op (1 op (2 op 0)) 

To illustrate point 2. consider:

evens = zipWith (+) nats nats
some = take 2 evens

We also recall that:

take 0 _ = []
take n (h:t) = h:(take (n-1) t)
 
zipWith op (x:xs) (y:ys) = (op x y):(zipWith xs ys)
zipWith _ _ _ = []

No expression is evaluated until we call some, in the interpreter. Thus, we start with the following un-evaluated expressions:

Variable Expression Value
evens ? unevaluated
some ? unevaluated
nats ? unevaluated

Upon calling some we obtain the following result which requires us to evaluate evens. This happens due to the pattern-matching definition of take, which requires a value of the form (x:xs).

evens ? unevaluated
some take 2 evens unevaluated
nats ? unevaluated

To evaluate evens, as before, the pattern-matching definition of zipWith requires the first element of nats:

evens zipWith (+) nats nats unevaluated
some take 2 evens unevaluated
nats ? unevaluated

Note that we only require x and y to evaluate zipWith in one step, hence (map (+1) nats) is not (yet) evaluated.

evens zipWith (+) nats nats unevaluated
some take 2 evens unevaluated
nats 0:(map (+1) nats) unevaluated

Thus, the evaluation of zipWith yields:

evens (0+0):zipWith (+) xs ys unevaluated
some take 2 evens unevaluated
nats 0:(map (+1) nats) unevaluated

Although we have created additional column in the table, we stress that the expressions (map (+1) nats) which appear in the body of nats, xs and ys are actually the same, and not different identical expressions. The first-step evaluation of take 2 evens is now complete:

evens (0+0):zipWith (+) xs ys unevaluated
some 0:(take 1 t) unevaluated
nats 0:(map (+1) nats) unevaluated
xs,ys (map (+1) nats) unevaluated
t zipWith (+) xs ys unevaluated

As before, note that both occurrences of zipWith (+) xs ys are actually the same expression. We continue with another step in the evaluation of take, which leads to evaluating zipWith (+) xs ys, and subsequently, xs and ys:

evens (0+0):zipWith (+) xs ys unevaluated
some 0:(take 1 t) unevaluated
nats 0:1:(map (+1) nats) unevaluated
xs,ys 1:(map (+1) nats) unevaluated
t zipWith (+) xs ys unevaluated

Note that after evaluating the expression (map (+1) nats) in one step, the expression is not re-evaluated, as shown in the table above.

evens (0+0):zipWith (+) xs ys unevaluated
some 0:(take 1 t) unevaluated
nats 0:1:(map (+1) nats) unevaluated
xs,ys 1:(map (+1) nats) unevaluated
t (1+1):zipWith (+) xs' ys' unevaluated

We have now finished the second step in the evaluation of take. We omit adding variables xs', ys' and t'.

evens (0+0):zipWith (+) xs ys unevaluated
some 0:2:(take 0 t') unevaluated
nats 0:1:(map (+1) nats) unevaluated
xs,ys 1:(map (+1) nats) unevaluated
t (1+1):zipWith (+) xs' ys' unevaluated

Now, take 0 t' evaluates to [], hence we finally get:

evens (0+0):zipWith (+) xs ys unevaluated
some 0:2:[] evaluated
nats 0:1:(map (+1) nats) unevaluated
xs,ys 1:(map (+1) nats) unevaluated
t (1+1):zipWith (+) xs' ys' unevaluated

and the evaluation stops.

Applications of normal evaluation

Dynamic programming - edit distance

Consider two strings s1 and s2. We define the edit distance between s1 and s2 as the minimal number of edit operations which make the strings identical. The allowed edit operations are:

As an example, the strings maple and apple have edit distance 2:

Dynamic programming computes the edit distance between two strings by building a matrix d having size(s1)+1 lines and size(s2)+1 columns, where d[i][j] represents the edit distance between the substrings s1[0:i] and s2[0:j]:

Functional implementation

Dynamic programming (for edit distance) can be efficiently implemented in Haskell, by exploiting lazyness in order to compute only those necessary distances, and only once. We first start with an example of an implementation of the infinite list of Fibonacci numbers:

fibo = 0:1:(zipWith (+) fibo (tail fibo))

References