Introduction to Functional Programming

Russ Ross

Computer Laboratory
University of Cambridge
Lent Term 2005


Lecture 9

Strict evaluation

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.

Lazy evaluation

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

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

Pipelines

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.

Lazy lists

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.

Lazy lists in SML

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

An infinite sequence

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

Consuming a sequence

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]

A filter

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

Joining two sequences

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

Functionals for lazy lists

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 xf(x), f(f(x)), …

- val from = iterates (fn x => 1+x);
> val from = fn : int -> int seq

Functionals for lazy lists

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

Searching trees

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

Depth first search

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

Breadth first search

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

Searching infinite trees

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.

Lazy searches

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