CSCI 275
Lab 8: Streams
Due Friday, May 8
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 liss are manipulated
with car, cdr, and cons.
Here
are the files you need to download for this lab: streams.rkt keyboard.rkt
Part 1: 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$, streams.rkt
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
You can test these out on the following stream:
(define Tester$ (cons$ 1
(cons$ 2 (cons$ 3 Tester$))))
Part 2: Mathematical Streams
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 first
row. A better approach is to enumerate the elements diagonally. The first
diagonal (right-to-left) contains only 1.1 The next diagonal contains 1.2 and 2.1 The
next 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 allPairs$
(pairsFrom$ (cons 1 1)))
Of course there is a missing piece here, which you will fill in on the next
exercise:
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. If you make nextPair
correctly the allPairs$ stream given above will work.
Exercise 3: Hamming sequences
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 Ham$ and notice the following facts about it.
Now all we have to do is combine elements
from these sources.
>
(print$ Ham$)
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 >
Part 3: 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:
Suppose
we generate the following streams of coefficients:
e-coeffs$ = (1/0! 1/1! 1/2! ... 1/n! ... )
sin-coeffs$ = (0 1/1! 0 -1/3! 0 1/5! ... )
cos-coeffs$ = (1 0 -1/2! 0 1/4! 0 -1/6! ...)
(Powers$ x) = (1 x x2 x3
x4 ....)
Mathematicians use the
term "Partial Sums" for the sequence of sums of the first n terms of a sequence. For the sequence a b c d e ... the
partial sums are a a+b a+b+c a+b+c+d ..... We
can write a procedure to make the stream of Partial Sums of the stream s:
(define
PartialSums$ (lambda (s)
(cons$
(car$ s) (+$ (PartialSums$ s) (cdr$
s)))))
Then
(PartialSums$
(*$ (Powers$ x) sin-coeffs$))
gives increasingly good approximations to sin(x), and the
other series act similarly.
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-coefffs$ can be built as follows::
(define e-coeffs$
(cons$ 1 (map$ (lambda (x) (/ 1.0 x)) fact-stream$)))
For
example, the first few terms of the e-coeffs$ are
1
1 1/2 1/6 1/24
1/120 1/720 1/5040 1/40320 1/362880
The
following function:
(define E (lambda (x) (PartialSums$ (*$ (Powers$ x) e-coeffs$))))
gives
increasingly good approximations to the values of the exponential function. For
example, (E 1) gives values 1
2.0 2.5 2.66 2.708 2.7166 2.7180
2.71825 ... Thanks to being bored once in high school I know that the
correct value to 14 decimal places is 2.71828182845904...
Exercises 4 and 5:
Produce the streams
sin-coeffs$ = (0 1/1! 0 -1/3! 0 1/5! ... )
cos-coeffs$ = (1 0 -1/2! 0 1/4! 0 -1/6! ...)
For these exercises you
only need to define the coefficient streams, but if you make the further
definitions:
(define
sin (lambda (x) (PartialSums$ (*$ (Powers$ x) sin-coeffs$))))
(define cos (lambda (x) (PartialSums$ (*$ (Powers$ x) cos-coeffs$))))
you should get increasingly accurate approximations to sin(x) and
cos(x).
Part 4: Filtering
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 to keyboard-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 >