ML uses strict, or eager, evaluation, more properly called call-by-value. The argument to a function is evaluated and the resulting value is substituted for the parameter name as the function is evaluated.
To evaluate
f (E1,…,En)
,
first evaluate
(E1,…,En)
,
and then apply f
to the resulting value.
In lazy evaluation, or call-by-name, the expressions
E1,…,En
are substituted into the definition of f
and the
resulting expression is evaluated.
Expressions aren't evaluated until they are matched against a pattern
- fun range 0 = [] | range n = n :: range (n - 1); > val range = fn : int -> int list - fun sum [] = 0 | sum (x::xs) = x + sum xs; > val sum = fn : int list -> int - sum (range 5); > val it = 15 : int
Call-by-need is similar, but duplicate expressions are only evaluated once. Haskell is the most widely used call-by-need language.
Lazy evaluation of sum (range 5)
proceeds like
this
sum (range 5) sum (5 :: range (5 - 1)) 5 + sum (range (5 - 1)) 5 + sum (range 4) 5 + sum (4 :: range (4 - 1)) 5 + (4 + sum (range (4 - 1))) 5 + (4 + sum (range 3)) 5 + (4 + sum (3 :: range (3 - 1))) 5 + (4 + (3 + sum (range (3 - 1)))) 5 + (4 + (3 + sum (range 2))) 5 + (4 + (3 + sum (2 :: range (2 - 1)))) 5 + (4 + (3 + (2 + sum (range (2 - 1))))) 5 + (4 + (3 + (2 + sum (range 1)))) 5 + (4 + (3 + (2 + sum (1 :: range (1 - 1))))) 5 + (4 + (3 + (2 + (1 + sum (range (1 - 1)))))) 5 + (4 + (3 + (2 + (1 + sum (range 0))))) 5 + (4 + (3 + (2 + (1 + sum [])))) 5 + (4 + (3 + (2 + (1 + 0)))) 5 + (4 + (3 + (2 + 1))) 5 + (4 + (3 + 3)) 5 + (4 + 6) 5 + 10 15
The programs we've examined so far have been entirely sequential. We enter an expression with all necessary arguments, the runtime system computes for a while, and a value is returned.
Many programs are reactive: they respond to the environment. They receive as much input as necessary to produce some output. The input could be an external event, such as a mouse click or keypress, or it could be the output of another reactive program unit.
In a lazy language, every program works this way—consumers pull values from producers
Producer => Filter => … => Filter => Consumer
Producers and filters generate data on demand; filters and consumers demand any needed data from other filters.
A lazy list, also called a stream, is a list of possibly infinite length
In SML we can implement lazy lists by delaying evaluation of the tail. This is weaker than true lazy evaluation, but gives us some of the benefits.
Recall the empty tuple ()
with type unit
To delay the evaluation of an expression E
, we can
use a lambda expression
fn () => E
.
- datatype 'a seq = Nil | Cons of 'a * (unit -> 'a seq);
Compare this with the datatype for normal lists
- datatype 'a list = nil | op:: of 'a * 'a list;
Cons(h,tf)
is the sequence with head h
and tail function tf.
- fun head (Cons(x,_)) = x; > val 'a head = fn : 'a seq -> 'a - fun tail (Cons(_,xf)) = xf (); > val 'a tail = fn : 'a seq -> 'a seq
We can use lazy lists to implement an infinite sequence
- fun from k = Cons(k, fn () => from (k+1)); > val from = fn : int -> int seq
This gives us the infinite sequence k, k+1, k+2, …
- from 1; > val it = Cons(1, fn) : int seq - tail it; > val it = Cons(2, fn) : int seq - val three = tail it; > val three = Cons(3, fn) : int seq
Note that these are still immutable values
- tail three; > val it = Cons(4, fn) : int seq - tail it; > val it = Cons(5, fn) : int seq - tail three; > val it = Cons(4, fn) : int seq
To collect the first n elements from a sequence into a list:
- fun get 0 s = [] | get n Nil = [] | get n (Cons(x, xf)) = x :: get (n-1) (xf ()); > val 'a get = fn : int -> 'a seq -> 'a list
A sample evaluation:
get 2 (from 6) get 2 (Cons(6, fn () => from 7)) 6 :: get 1 (from 7) 6 :: get 1 (Cons(7, fn () => from 8)) 6 :: 7 :: get 0 (from 8) 6 :: 7 :: get 0 (Cons(8, fn () => from 9)) 6 :: 7 :: [] [6, 7]
We can transform one sequence into another
- fun squares Nil = Nil | squares (Cons(x,xf)) = Cons(x*x, fn () => squares (xf ())); > val squares = fn : int seq -> int seq - squares (from 1); > val it = Cons(1, fn) : int seq - get 5 it; > val it = [1, 4, 9, 16, 25] : int list
Or we can generalize simple transformations
- fun mapq f Nil = Nil | mapq f (Cons(x,xf)) = Cons(f x, fn () => mapq f (xf ())); > val ('a, 'b) mapq = fn : ('a -> 'b) -> 'a seq -> 'b seq - mapq (fn x => x*x) (from 1); > val it = Cons(1, fn) : int seq - get 5 it; > val it = [1, 4, 9, 16, 25] : int list
A simple way to join two sequences is analogous to the append function for lists
- fun appendq (Nil, yf) = yf | appendq (Cons(x,xf), yf) = Cons(x, fn () => appendq (xf (), yf)); > val 'a appendq = fn : 'a seq * 'a seq -> 'a seq
However, if the first sequence is infinite, then
appendq (x, y) = x
. Instead,
we can alternate taking one element from each sequence
- fun interleave (Nil, yf) = yf | interleave (Cons(x,xf), yf) = Cons(x, fn () => interleave (yf, xf ())); > val 'a interleave = fn : 'a seq * 'a seq -> 'a seq
We've already seen mapq
. We can write the equivalent
of foldl
if we know the sequence is finite, but it isn't
too safe in general.
We can generalize our from
function
- fun iterates f x = Cons(x, fn () => iterates f (f x)); > val 'a iterates = fn : ('a -> 'a) -> 'a -> 'a seq
iterates
gives the infinite sequence
x, f(x), f(f(x)), …
- val from = iterates (fn x => 1+x); > val from = fn : int -> int seq
We can use filterq
to contract sequences
- fun filterq p Nil = Nil | filterq p (Cons(x,xf)) = if p x then Cons(x, fn () => filterq p (xf ())) else filterq p (xf ()); > val 'a filterq = fn : ('a -> bool) -> 'a seq -> 'a seq
As we saw with our Huffman encoder, there are many ways to search a tree. One main way to classify search algorithms is by the order in which they search elements
DFS is fast and simple, requiring space proportional to the height of the tree being searched. Here we raise an exception internally when we find a solution to stop the search immediately. The function is slightly more complex than necessary in order to explicitly show the stack of nodes to be searched
fun dfs p tree = let fun f [] = () | f (Lf::xs) = f xs | f (Br(x,l,r) :: xs) = if p x then raise Success x else f (l :: r :: xs) in (f [tree]; raise Not_found) handle Success x => x end
BFS finds the nearest solution to the root, and requires space proportional to the size of the tree
fun bfs p tree = let fun f [] = () | f (Lf::xs) = f xs | f (Br(x,l,r) :: xs) = if p x then raise Success x else f (xs @ [l,r]) in (f [tree]; raise Not_found) handle Success x => x end
Many applications involve searching a large search space, represented as a tree. In many cases the tree is not available as a static structure, but the search still follows a conceptual tree structure.
We can represent such a tree as a function of type
next: 'a -> 'a list
The function takes a node and returns a list of the children of that node. For example, if the problem was to solve a puzzle, the function might take a single position of the puzzle and return all positions that could be reached by a single move from that position.
Our searches can return a lazy sequence of nodes in the tree.
A DFS may never find a solution even if one exists, but it may also find the solution quickly
- fun depth next root = let fun f [] = Nil | f (x::xs) = Cons(x, fn () => f (next x @ xs)) in f [root] end; > val 'a depth = fn : ('a -> 'a list) -> 'a -> 'a seq
A BFS will always find a solution in finite time if it exists, and it finds the nearest solution to the root
- fun breadth next root = let fun f [] = Nil | f (x::xs) = Cons(x, fn () => f (xs @ next x)) in f [root] end; > val 'a breadth = fn : ('a -> 'a list) -> 'a -> 'a seq