Recitation 18: More streams examples

  1. Fibonacci sequence
  2. fromList
  3. circular
  4. map
  5. takeN
  6. sfold: An infinite, moving fold over a stream
Here are more examples of streams. Recall that streams are defined in a very similar way as standard lists, except that the tail of the list is always thunkified with an anonymous function. This fact allows one to convert a expression consisting of lists into a stream with the standard transformation of thunkifiying the tails. One way to help you write infinite streams is to think of a recursive expression for the list, and then use this expression to write an infinitely recursing function to constructs the list.

Note that redefining cons to operate on lists and stripping out the fn() => in the thunkifying idiom fn() => EXPR will result in valid SML code for a list version of the stream. Of course, if the stream is infinite, then SML will not be able to evaluate the list to a value.

For these examples, we will use the standard definition of streams.

datatype 'a stream = Null
  | Cons of 'a * (unit -> 'a stream)

fun cons (x) (y) = Cons(x,y)

Fibonacci sequence

fun fibo(a: int, b: int) = cons a (fn () => fibo(b, a + b))

The invariant here is that fibo(x,y) always generates a sequence, starting with x,y that follows the fibonnaci recurrence -- each element is the sum of two previous elements.

fromList

fromList converts a list into a stream.

fun fromList(l: 'a list): 'a stream =
  case l of
    []   => Null
  | h::t => Cons(h, fn () => fromList t)

fromList(l) is the stream version of a function that copies a list into newly-constructed cells.

circular

circular takes a list and defines an infinite stream consisting of an infinite number of occurances of the input list.

 fun circular(l: 'a list): 'a stream =
  case l of
  [] => Null
  | h::t => Cons(h, fn () => circular (t @ [h]))

This solution generates an infinite loop by repeatedly calling circular with the same length list. The new list is generated such that the result values are returned in the same order as in the original list.

map

fun map (f: 'a -> 'b) (s: 'a stream): 'b stream =
  case s of
    Null  => Null
  | Cons(h, t) => Cons(f(h), (fn () => map f (t())))

takeN

Here is the original definition of takeN, which returns the first n elements of a stream as a list.

fun takeN(s: 'a stream) (n: int): 'a list =
  case (s, n) of
    (_, 0)          => []
  | (Null, _)       => raise Empty
  | (Cons(h, t), n) => h :: (takeN (t()) (n - 1))

As defined above, takeN is restricted to only seeing the first part of the list. The following definition of takeN returns the stream with the first n elements removed.

fun takeUpToN(s: 'a stream) (n: int): ('a list * 'a stream) =
    case (s, n) of
	(_, 0) => ([], s)
      | (Null, _) => ([], s)
      | (Cons(h, t), n) => 
	    let 
		val (l, s) = takeUpToN (t()) (n - 1)
	    in
		(h :: l, s)
	    end

fun takeN(s: 'a stream) (n: int): ('a list * 'a stream) =
    let
	val (l,rest) = takeUpToN s n
    in
         (* We can avoid this call to length by refactoring takeUpToN *)
	if (length l) = n then
	    (l, rest)
	else
	    raise Empty
    end

This version of takeN allows you to conveniently process a stream in large chunks by converting these chunks into lists. To do so, one would invoke takeN with the result stream to extract the next n elements.

sfold: An infinite, moving fold over a stream

Let's look at a variant of fold that is well-defined even for an infinite stream. How can we define fold on an infinite stream? If we return only a single value, then we can only process a finite portion of the stream. Otherwise, we would potentially have to visit all elements of the stream to compute that value, which takes infinite time.

Creating an infinite output stream as the result allows us to define a fold operation that can use all elements in the infinite stream. Each individual element of the output stream can only depend on a finite number of elements in the input stream. However, since the output stream is infinite, we may eventually visit each input node.

One way of defining stream fold is to compute fold over a moving window of length lookahead. For an output element at position k, the value is taken from the fold of the elements at positions k through k + lookahead - 1.

Here is a solution that uses the takeUpToN helper function used in takeN. takeUpToN performs the same operation as takeN, but only with as many elements as available. If we used takeN, then sfold would run into problems near the end of a finite stream.

We could define different semantics from sfold, perhaps disallowing finite streams, or throwing an exception when attempting to evaluate elements near the end of the finite stream.

fun ('a, 'b)
  sfold (lookahead: int, f: 'a * 'b -> 'b, acc: 'b, s: 'a stream): 'b stream = 
    let
	val (l, _) = takeUpToN s lookahead
    in
	Cons(foldr f acc l, 
	     fn() => sfold(lookahead, f, acc, (tl s)))
    end