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.