SICP 6b
Streams II

Table of Contents

1 How long is a piece of string?

On-demand computation is built into streams. Let's recap.

(define (nth-stream n s)
  (if (= n 0)
      (head s)
      (nth-stream (-1+ n) (tail s))))

(define (print-stream s)
  (cond ((empty-stream? s) "done")
        (else (print (head s))
              (print-stream (tail s)))))

A stream can be infinitely long.

(define (integers-from n)
  (cons-stream
   n
   (integers-from (+1 n))))
(define integers (integers-from 1))

(define (no-seven x)
  (not (= (remainder x 7) 0)))
(define ns (filter no-seven
                   integers))

If something is there whenever you look, is it really there or not?

2 Sieve of Eratosthenes

We saw Hero of Alexandria's method for calculating square roots in lecture 1a. Eratosthenes was a fellow Alexandrian, who created a method for calculating primes.

(define (sieve s)
  (cons-stream
   (head s)
   (sieve (filter
           (lambda (x)
             (not
              (divisible? x (head s))))
           (tail s)))))
(define primes
  (sieve (integers-from 2)))

sieve is an infinitely recursive filter.

3 All-in-one inclusive

Procedures may process streams in one go. Take a look at two handy procedures.

(define (add-streams s1 s2)
  (cond ((empty-stream? s1) s2)
        ((empty-stream? s2) s1)
        (else
         (cons-stream
          (+ (head s1) (head s2))
          (add-streams (tail s1)
                       (tail s2))))))

(define (scale-stream c s)
  (map-stream (lambda (x) (* x c)) s))

Using these two procedures, we can create procedures that process streams all at once.

(define integers
  (cons-stream 1
               (add-stream integers ones)))

This looks an awful lot like a finite state accumulator. We can use the same trick for integrals.

(define (integral s initial-value dt)
  (define int
    (cons-stream
     initial-value
     (add-streams (scale-stream dt s)
                  int)))
  int)

And also for Fibonacci numbers.

(define fibs
  (cons-stream 0
     (cons-stream 1
        (add-streams fibs (tail fibs)))))

4 Drawing hands

As we have recursive procedures, it's natural we have recursive data too. We can still run into recursive problems however.

(define y
  (integral
   dy 1 .001))
(define dy
  (map square y))

This doesn't work due to the recursive definition. There is a solution however.

(define (integral delayed-s
                  initial-value
                  dt)
  (define int
    (cons-stream
     initial-value
     (let ((s (force delayed-s)))
       (add-streams (scale-stream dt s)
                    int))))
  int)

Change the integral procedure to expect integrand to be a delayed object. Then just need one more minor change.

(define y
  (integral
   (delay dy) 1 0.001))

5 Normal-order evaluation

The divorce of time in programs from time in computers has yielded powerful methods of decomposition, all of which have remained hidden within streams until now. By taking full advantage of this new method, we have started to pull out the innards of streams, explicitly calling delay and force.

Spaghetti code can be avoided, by changing the language so that all procedures act like cons-stream. In normal-order evaluation languages (in constrast to applicative-order evaluation languages), every procedure has an implicit delay around its arguments. Streams wouldn't be needed, since lists would be streams already. Miranda, by David Turner from the University of Kent, is a normal-order evaluation language.

There is, however, a price.

We're trying to think about programming as a way to specify processes. And if we give up too much time, our language becomes more elegant, but it becomes a little bit less expressive. There are certain distinctions that we can't draw.

One of the disadvantages is that we can't really express iteration.

(define (fact-iter a)
  (define (iter product counter)
    (if (> counter n)
        product
        (iter (* counter product)
              (+ counter 1))))
  (iter 1 1))

With normal-order evaluation, the above code would run nowhere near as efficiently. The dragging tail problem is an issue people writing operating systems are faced with.

A more striking issue is that normal-order evaluation and side effects just don't mix. It's impossible to model objects with local state and change, and at the same time conjure normal-order tricks of decoupling time.

(define x 0)
(define (id n)
  (set! x n)
  n)
(define (inc a) (1+ a))
(define y (inc (id 3)))
x
0
y
4
x
3

The whole idea of putting in delays is that you throw away time. But when we talk about state and set and change, that's exactly what we do want control of.

A so-called purely functional language has no side effects. The price is losing assignment.

Let's take an example which would be naturally thought of in terms of object, and approach it with a functional frame of mind.

(define (make-deposit-account
         balance deposit-stream)
  (cons-stream
   balance
   (make-deposit-account
    (+ balance (head deposit-stream))
    (tail deposit-stream))))

We don't know if we can do everything without assignment, but there seem to be places where functional programming breaks down. Where it starts hurting is with objects and sharing and two independent agents being the same.

Say if we were to extend the bank account so that two users shared the same account. The streams could be merged. Fair merge would alternately switch between each user, and if one is empty at any point, take from the other stream twice. However notice that we can't even explain the situation without talking about time.

Note: it may be that with fair merge, assignment can be effectively simulated.