CS275

Lab 09

Streams


This is due on Wednesday,
November 30

In this lab we will be working with streams. A stream is a data structure made possible by a new version of cons called cons$, which does not evaluate its second argument. Thus a stream consists of a head element, which can be accessed in the usual way using the car$ function and a tail which is a "promised" stream. The tail is accessed via the cdr$ function which forces evaluation of the original second argument to cons$.

You can think of streams as infinite lists which are manipulated with car$, cdr$, and cons$ in much the same way as finite lists are manipulated with car, cdr, and cons.

Here are the files you need to download for this lab::

streams.rkt    keyboard.rkt

Section 1: Laziness

Here are some new Scheme expressions:

  • (delay exp) returns an object called a promise, without evaluating exp.
  • (force promise) evaluates the promised expresson and returns its value.

If a promised expression has been evaluated once, forcing it again returns its value without re-evaluating it..

For example,

(define foo
      (delay (begin
                        (display "Oh, goody I'm being evaluated!\n")
                        2)))

If we force the evaluation of foo with

(force foo)

the system responds with

"On goody I'm being evaluated!
2

If we force foo again, the system only responds with

2

the value of this expression has been recorded and returned; the expression is not re-evaluated.

This is called "lazy" or "just in time" evalation. While Scheme normally wants to evalate all expressions immediately (even lambdas, which evalate to closures), delay puts evaluation off until we explicitly force it.

Section 2: Streams

A stream is a list-like structure, with some of the values actually present and the rest contained in a promise.   This can be used to represent infinite sequences.  For example, we might  have a stream of positive integers:

  (1 2 3 <promise>)

Each time we evaluate the promise we get the next value of the stream, and another promise to produce the rest:

  (1 2 3 4 <new promise>)

Streams are built from three primitive operators:

These three are contained in file "streams.rkt" which we will use for all of our work with streams. You want to download the file streams.rkt and put the line

(require "streams.rkt")

at the top of any Dr. Racket file using streams.

In addition to car$, cdr$ and cons$, stream.ss defines a number of helper procedures for working with streams:

Here are a few examples:

Here is another, more subtle definition of the integers starting from 1:

The first elements of Ints1$ is certainly 1, since we cons$ 1 onto the front of the stream. The next element is the result of adding the first element of Ones$ (i.e, 1) onto the first element of Ints1$, which we just saw is also 1; so the second element is 2. The next elment is the result of adding 1 onto this second element to get 3, and so forth.

Exercise 1

Write the functions

  • (rember-all$ x s); returns a stream with all occurrences of x removed from the stream s
  • (subst-all$ x y s); returns a stream with all occurrences of x in the stream s replaced with

Section 3: Streams of Numbers

Remember our stream construction of the integers:

A good way to produce streams of numbers is to write a function which produces the next number in the stream given the current number. That is the technique used just now in IntsFrom$: A call to (IntsFrom$ n) does a cons$ of n onto a recursive call to IntsFrom$ passing it the next number (+ n 1).

Here is a challenge. Consider the following grid of pairs of numbers:

1.1 1.2 1.3 1.4 1.5 1.6 1.7 ... 
2.1 2.2 2.3 2.4 2.5 2.6 2.7 ...  
3.1 3.2 3.3 3.4 3.5 3.6 3.7 ...  
4.1 4.2 4.3 4.4 4.5 4.6 4.7 ...  
5.1 5.2 5.3 5.4 5.5 5.6 5.7 ...  
6.1 6.2 6.3 6.4 6.5 6.6 6.7 ...  
7.1 7.2 7.3 7.4 7.5 7.6 7.7 ...   
 .   .   .   .   .   .   .  

Note that we can display a.b with (cons a b). Of course, it is easy to make the first row of this grid as a stream, but if we try to print the grid row-by-row we will never finish with the fist row. A better approach is to enumerate the elements diagonally. The first diagonal (right-to-left) contains only 1.1 The next diagonal containts 1.2 and 2.1 The enxt 1.3  2.2 and 3.1 And so forth.

We can define this stream of pairs pretty easily, the same way we define our stream of integers:

(define pairsFrom$
     (lambda (p)
          (cons$ p (pairsFrom$ (nextPair p)))))

(define pairs$ (pairsFrom$ (cons 1 1)))

Of course there is a missing piece here:

Exercise 2

Write procedure (nextPair p) that takes one of the pairs for this enumeration and returns the next pair. This is not itself a stream function; it is just what we use to make our stream of pairs.

 

Exercise 3

A famous problem, first raised by R. Hamming, is to enumerate, in ascending order with no repetitions, all positive integers with no prime factors other than 2, 3, or 5. One obvious way to do this is to simply test each integer in turn to see whether it has any factors other than 2, 3, and 5. But this is very inefficient, since, as the integers get larger, fewer and fewer of them fit the requirement. As an alternative, let us call the required stream of numbers S and notice the following facts about it.

  • S begins with 1.
  • The elements of (scale S 2) are also elements of S.
  • The same is true for (scale S 3) and (scale 5 S).
  • These are all the elements of S.

Now all we have to do is combine elements from these sources.

  1. Write (merge$ s1 s2), which combines two ordered streams s1 and s2 into one ordered result stream, eliminating repetitions.
  2. Construct the desired stream S.

You should be able to do the following with S:

> (print$ S)  
1 2 3 4 5 6 8 9 10 12   
Want more? y  
15 16 18 20 24 25 27 30 32 36   
Want more? y  
40 45 48 50 54 60 64 72 75 80   
Want more? y  
81 90 96 100 108 120 125 128 135 144   
Want more? n    
done  > 

Section 4: Power Series

Streams can be used to model the power series that define important mathematical functions. Among these are the following that compute the exponential, sine and cosine functions:

 
 


We will represent the series a0 + a1 x + a2 x2 + a3 x3 + ··· as the stream whose elements are the coefficients a0, a1, a2, a3, ....:

e-series$ = (1/0! 1/1! 1/2! ... 1/n! ... )  
sin-series$ = (0 1/1! 0 -1/3! 0 1/5! ... )  
cos-series$ = (1 0 -1/2! 0 1/4! 0 -1/6! ...)  

To define these streams we start by building the stream of factorials. First we need *$, defined analogously to +$: it takes 2 numerical streams and multiplies them element by element

       (define *$
           
 (lambda (s1 s2)
                     (cons$ (* (car$ s1) (car$ s2)) (*$ (cdr$ s1) (cdr$ s2)))))

Now notice that


fact-stream$ = 1 1 (2 * 1) (3 * 2 * 1) ... *$ (IntsFrom$ 1) = 1 2 3 4 ... ------------------------------------------------- 1 (2 * 1) (3 * 2 * 1) (4 * 3 * 2 * 1) ...
The last line is just (cdr$ fact-stream$). Consequently we can define fact-stream$ as follows.
(define fact-stream$    
     (cons$ 1 (*$ fact-stream$ (IntsFrom$ 1))))

Now we can build our power series.

The e-series$ can be built by applying the following function to the stream of factorials:

(define e-summands    
      (lambda (s)
            (cons$ (/ 1 (car$ s)) (e-summands (cdr$ s)))))
(define e-series$ (e-summands (fact-stream$)))

For example, the first few terms of the e-series$ are

1  1  1/2  1/6  1/24  1/120  1/720  1/5040  1/40320  1/362880

Exercises 4 and 5:

Produce the power series for

sin-series$ = (0 1/1! 0 -1/3! 0 1/5! ... )  
cos-series$ = (1 0 -1/2! 0 1/4! 0 -1/6! ...)  

 

Section 5: Communication Streams

File keyboard.rkt has code that makes a keyboard-stream. This is a stream of whatever s-expressions you type. There is also an output$ function that takes a stream and outputs it one element at a time. Together these set up a communication channel:

(output$ (keyboard-stream))

echoes whatever you type, and continues until you click on the "eof" box at the right. We will use this in the next set of exercises:

Grune's Problem

There is a famous problem due to Grune that is important in data communications. Consider a stream of characters, which we will model by typing single characters followed by a carriage-return in response tokeyboard-stream's prompts. Most characters we will allow to pass through unaltered. But if we ever receive two a's in a row, then we will pass through just a single b and no a.

Exercise 6

Write the filter grune-a-b to work as follows:

> (output$ (grune-a-b (keyboard-stream)))  
? a
? b
a b
? c
c ? d d ? a ? a b ? a ? b a b ? a ? a b ? <clicks eof> done >

Exercise 7

Now write a similar function, but abstract the a and b. Write a function grune which takes two arguments and produces a filter similar to grune-a-b but with the first argument taking the role of a and the second taking the role of b. Your code should start:

(define grune    (lambda (a b)   

where now a and b are variables. (grune 'x 'y) needs to return a function, similar to grune-a-b which works as a stream filter replacing two successive x by a single y. Here is a transcript of grune in action:

> (output$ ((grune 'x 'y) (keyboard-stream)))  
? a  
a  
? b  
b  
? c  
c  
? d  
d  
? x  
? y  
x  
y  
? a  
a  
? b  
b  
? x  
? x  
y  
? x  
? x  
y  
? a  
a  
? b  
b  
? <clicks eof>  
done  >   

Notice that (grune 'a 'b) has the same functionality as grune-a-b.



Grune's problem becomes interesting when we consider pipelining several Grune operations. If you did the last exercise correctly, you should be able to chain several "grune"s together:

> (output$ ((grune 'c 'd) ((grune 'b 'c) ((grune 'a 'b) (keyboard-stream)))))   
? a  
? b  
a  
? c  
b  
? d  
c  
d  
? e  
e  
? f  
f  
? g  
g  
? a 
? a  
? a  
? a  
? a  
? a  
? a  
? a  
d  
? a  
? a  
? a  
? a  
? x  
c  
x  
? y  
y  
? z  
z  
? x  
x  
? y  
y  
? z  
z  
? <clicks eof>  
done  >