PS#4 is now due on Friday at 6:59PM.
Consider
the question:
"What is the second prime between
42,000 and 42,000,000?"
hd(tl(filter prime (enumerateInterval 42000 42000000)))
With streams, only as much computation is done as is necessary to provide the next output. (Actually, this isn’t strictly true, since the head gets forced, but it’s pretty close).
Anyway, as a result if we do this with streams:
hdStream (tlStream
(filterStream prime (enumerateIntervalStream 42000 42000000)))
we only do the computation that we really need. We never get to the integers out around 42000000.
So, we could actually make that last number arbitrarily large and we’d get the same behavior.
Another way to see this: with lists, the last number is there to stop the recursion from going on indefinitely. With streams, the delay has already stopped the recursion!
fun const(c: 'a) = consStream c (fn() => const(c));
val repeat = const “repeat”
takeStream repeat 3 (* [“repeat”, “repeat”, “repeat”] *)
Compare this with a version of const that called const(c) directly; this would loop when we call it (not, of course, during function definition…).
OK, let’s try something more interesting:
fun nats(n: int) = consStream n (fn () => nats(n + 1));
takeStream (nats 4) 2 (* [4,5] *)(nats 4) is bound to something like Cons(4,[promise: nats 5])
tlStream (nats 4) is bound to something like Cons(5,[promise: nats 6]), etc.
Here’s an even better example:
fun fibo(a: int, b: int) = consStream a (fn () => fibo(b, a + b));
val fibs = fibo(0,1)
Intuitively, we take the second argument and shift it to the first argument, while adding it to the old first argument. [Note: fibs are 0,1,1,2,3,5,8,13,…]
Here, fibs is Cons(0, [promise: fibo(1,1)])
tlStream fibs is Cons(1,[promise: fibo(1,2)])
tlStream (tlStream fibs) is Cons(1,[promise: fibo(2,3)])
tlStream(tlStream (tlStream fibs)) is Cons(2,[promise: fibo(3,5)])
tlStream(tlStream(tlStream (tlStream fibs))) is Cons(3,[promise: fibo(5,8)])
In general we can apply normal stream operations to infinite streams, such as map and filter (obviously, fold would be a problem).
fun undivisible(p:int)(n:int) = n mod p <> 0;
val odds = filterStream (undivisible 2) (nats 1);It's easy to count by 3's. Let's do something
more interesting:
·
Sieve of Eratosthenes
(300 BC)
·
Build a stream of primes.
·
2 is prime.
·
A number n>2 is prime
iff it is not divisible by any prime number smaller than itself.
We can look at this as a recursive process,
the head is in the result, and
so is the tail filtered to remove anything
divisible by the head, and then
recursively.
fun sift (p: int) (s: int stream): int stream =
filterStream (undivisible p) s
fun sieve (s: int stream): int stream =
case s of
nilStream => nilStream
| Cons(s, t) => Cons(s, fn () => sieve(sift s (t())))
val primes = sieve(nats(2));
The head of the stream is 2, the tail is integers not divisible by 2. What is the tail of the tail? This is a good example to work through to make sure you understand the delays!
Finally, let us look at an abstraction that allows us to do a lot of things that we just did in a different (and more recursive) manner.
fun addStreams (s1: int stream) (s2: int stream): int stream =
if (nullStream s1) then s2 else
(if (nullStream s2) then s1 else
(consStream ((hdStream s1)+(hdStream s2))
(fn() => (addStreams (tlStream s1) (tlStream s2)))))
(* Why must this be a fun and not a val? *)
fun newonesfun() = consStream 1 newonesfun
val newones = newonesfun()
fun newintsfun() = consStream 1
(fn() => addStreams newones (newintsfun()))
val newints = newintsfun()
fun newfibs() = consStream 0
(fn() => consStream 1
(fn () => addStreams (newfibs()) (tlStream (newfibs()))))
When we ask for the third element, add-streams adds the first and second.
The tail is a promise to
addStreams
(tailStream newfibs()) (tailStream (tailStream newfibs()))
In this case we need two values to ``get the stream started''.
fun appendStreams (s1: 'a stream) (s2: 'a stream): 'a stream =
if (nullStream s1)
then s2
else consStream (hdStream s1) (fn() => appendStreams (tlStream s1) s2)
fun appendStreams2 (s1: 'a stream) (s2: 'a stream): 'a stream =
if (nullStream s1)
then s2
else consStream (hdStream s1) (fn() => appendStreams2 s2 (tlStream s1))takeStream (appendStreams newints primes) 10
(* [1,2,3,4,5,6,7,8,9,10] *)
takeStream (appendStreams2 newints primes) 10
(* [1,2,2,3,3,5,4,7,5,11] *)
val lose = filterStream odd
(filterStream (fn(x)=> not (odd x)) newints)(* runs forever, immediately! *)
A stream need not be infinite. Given a list of values, let's make it into a
stream:
fun fromList(l: 'a list): 'a stream =
case l of
[] => nilStream
| h::t => Cons(h, fn () => fromList t)
- take (fromList [9, 7, 5, 3, 1]) 4;val it = [9,7,5,3] : int list- take (fromList [9, 7, 5, 3, 1]) 10; uncaught exception Empty raised at: stdIn:140.30-140.35
A finite list of values can be made into an infinite stream by circularly
reusing the given elements:
fun circular(l: 'a list): 'a stream =
case l of
[] => nilStream
| h::t => Cons(h, fn () => circular (t @ [h]))- take (circular [1, 2, 3]) 10;
val it = [1,2,3,1,2,3,1,2,3,1] : int list