TONIGHT: PRELIM #2, Kimball B11, 7:30-9:00. Handed back in section tomorrow (a late night for the course staff!) No quick reference guides this time or on the final.
TODAY: More about streams:
Infinite streams
Pitfalls of delayed evaluation
Streams and functional vs. imperative programming
Last time:
* Programming paradigm - streams
* Same basic contract as a list:
(heads (cons-stream x str)) = x
(tails (cons-stream x str)) = str
* Order of evaluation is DIFFERENT
(cons-stream x str) --- evaluates x immediately
str only when it needs it.
(tails str) ----- forces evaluation of the tail of the stream.
Delayed Evaluation:
* Only compute values in the tail when they're needed.
* Use (delay foo) special form.
(delay foo) -- makes a promise to compute foo when forced to.
MAKE IOU. Could be (lambda () (foo))
(force foo) -- collects on the promise. Could be (foo)
There's one last inefficiency possible:
Each time it is computed anew.
Expensive!
We MEMOIZE it the first time we compute it ---
save result, use it later
Streams gave us a view of a program as SIGNAL PROCESSING:
* data going through a chain of boxes
"What is the second prime between 10000 and a zillion?"
+--------+ +--------+
(10000,zillion) -->|prime? | -->|second | ---> answer
+--------+ +--------+
The KEY difference between streems and lists is that with streams the
flow of data is like "pulling a string", only as much computation gets
done as is needed to provide the next output.
If the stream we've made doesn't do any computation, what's the point
of that zillion anyways?
* With lists, it'd be there to stop the recursion and keep the lists
finite
* With stream, the *delay* has already stopped the recursion
- Don't make more stream until you need it!
* So we don't need another device to stop it.
Just make the stream *infinitely* long!
- That is, whenever you take the tail of it
-- asking for the next value -- there'll be a next value.
(define integers-from
(lambda (n)
(cons-stream n (integers-from (+ 1 n)))))
(define integers (integers-from 1))
* integers is an infinite stream:
-- There's always another integer to pull off of it.
* But it's not an infinite *loop*:
-- No call to (integers-from 2), because cons-stream delays it.
-- compare with CONS
integers is bound to something that looks like:
( 1 . {promise (integers-from (+ 1 1))} )
Let's print some of this stream out:
(define print-stream
(lambda (s)
(print (heads s))
(print-stream (tails s))))
[Note: this is not (maps print s) – why?]
(print-stream integers)
* Prints 1
* Forces the tail, the promise,
- Evaluates (+ 1 1) to 2
- evaluates integers-from, giving us
(2 . {promise (integers-from (+ 1 2))})
* Prints 2
* Forces the tail...
- (3 . {promise (integers-from (+ 1 3))})
* Prints 3
<etc>
Stream lambdas MOSTLY make sense on infinite streams.
For example,
(define divisible?
(lambda (x n)
(= (remainder x n) 0)))
(define threes
(filters (lambda (x) (divisible? x 3))
integers))
Is a stream with the elements:
3 6 9 12 15 18 ...
No matter many you get out of the stream, there are always more.
PHILOSOPHY:
* Are all the numbers really there?
* Well, what does "really there" mean?
1. If you look at them, you'll find them.
2. But if you don't look at them, they're not explicitly
represented in the computer -- not as numbers anyways.
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.
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ...
^ ^ / ^ | ^ | | | ^ | ^ | | | ^ | ^ |
2 3 2 3 2 2 2 3 2 2 2
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.
(define sieve
(lambda (stream)
(cons-stream
(heads stream)
(sieve (filters (lambda (x)
(not (divisible? x (heads stream))))
(tails stream))))))
new tail is a stream not divisible by the current head
(define primes (sieve (integers-from 2)))
1. First in the stream is 2.
And the tail of the stream has all the integers not divisible by 2
[This is an example where you need to really understand the delays
to make sense of it.]
Now, something even more peculiar:
* Defining a stream in terms of itself.
We *didn't* do this with integers-from --
-- there we defined a procedure returning a stream.
(define ones (cons-stream 1 ones))
ones looks like 1 1 1 1 1 1 ...
ASIDE:
If we tried this with regular cons, it would not work -- WHY????????
-- it'd get the old value of ones and stick a 1 on that.
-- Or an error if there wasn't one.
But cons-stream delays the second argument
-- and it's as easy to delay `ones' as anything else.
Let's define adding streams:
(define add-streams
(lambda (a b)
(cond ((empty-stream? a) b)
((empty-stream? b) a)
(else (cons-stream (+ (heads a) (heads b))
(add-streams (tails a) (tails b)))))))
(define integers
(cons-stream 1
(add-streams ones integers)))
integers ---> (1 . {promise to (add-streams ones integers)})
(tails integers)
(2 . {promise to (add-streams (tails ones) (tails integers))} )
(tails (tails integers))
;; Add the 1 and 2
(3 . {promise to (add-streams (tails (tails ones)) (tails (tails integers)))})
This isn't very different from having a *lambda* defined in terms of
itself --- recursion.
Now, let's do something a bit more involved using integer streams: Fibonacci numbers
F(n)=F(n-1)+F(n-2)
F(0)=0
F(1)=1
(define fibs
(cons-stream 0
(cons-stream 1
(add-streams fibs (tails fibs)))))
When we ask for the third element, add-streams adds the first
and second. The tail is a promise to
(add-streams (tails fibs) (tails (tails fibs)))
In this case we need two values to ``get the stream started''.
There are some problems though:
It's very easy to make divergent computations:
- which are not the same as being empty
- And, worse, empty-stream? can't detect them!
(define lose
(filters odd? (filters even? integers)))
[Note: (heads lose) isn't what runs forever. filters always runs until
it finds a value for the head, so the odd filters will run forever.]
[Can we build a smart version of empty-stream? That detects divergent computations? NO. See last lecture.]
Delayed evaluation can cause all kinds of trouble when mixed with
assignment (set!)
* Kind of like drinking and driving.
(define x 'fun)
(define make-empty
(lambda ()
(set! x 'yow!)
empty-stream))
(define y (cons-stream 1 (make-empty)))
x ===> 'fun
y ===> {printed representation of a stream with head 1}
x ===> 'fun
(tail y) ===> empty-stream
x ===> 'yow!
This is really strange.
* Looking at some value (y) changed x.
* Side effects happen at all kinds of random times.
* When you print one variable to see what its value is, you might
change some other ones.
* You can't trust *anything*
Great Functional vs. Imperative Programming Debate:
Functional Programming:
Imperative programming:
(FP): Side effects cause trouble, in many ways. They mess up your
thinking. Give up the habit.
(IMP): I'll be good and just use side effects for local state, like o-o
style. No (or few) global variables.
But I really *need* that state. Can't do good OO programming without
it.
(FP): Have I got "state" for you! Look at a bank account as being an
infinite of stream of transactions and the resulting balances.
Each new transaction comes along and the balance changes
accordingly.
+--------+
transactions --> |ACCT | --> balances
+--------+
What about a shared account?
Well, we could have more than one input stream.
+--------+ +------+
Chris --->|MERGE | ----> | ACCT | --> balances
Pat --->| | +------+
+--------+
OK, but what's that MERGE critter like?
* Alternating is no good
- Then Chris can't deposit money and immediately withdraw it,
Pat has to do something in between, and maybe Pat isn't even at
the bank.
* Fair merge -- ask each one if they're ready to give input.
- If one is ready and the other isn't, take the one that is.
- Otherwise, alternate or something.
Note: notion of *time* has re-entered
But this merge box knows about state!
The debate isn't over.
We don't know *what* to do, actually.
Summary:
* Delayed evaluation is a powerful tool for allowing certain
abstractions to be efficient.
* Delayed evaluation works badly with set!
* You can often view state as time-evolution of a process
- and package it in a stream
instead of as explicit state.