5/5.1 Streams. Streams Are Delayed Lists

Is there a alternative approach to assignment when it comes to modeling state? Think of the way a mathematical function of time x(t) describes the time-varying behavior of a quantity x. In a certain sense, it describes the history of values. Streams, the new data structure introduced in this section, allow us to ``model change in terms of sequences that represent the time histories of the systems being modeled.'' So, yes. Systems with state can be modeled without ever using assignment (or mutable data). Assignment's drawbacks are sidestepped, but new challenges show up.

Streams Are Delayed Lists

Sequences have proven themselves useful for formulating powerful abstractions (think of map, filter, etc.). However, representing sequences as lists means buying elegance at the price of (space and time) inefficiency. Think of how outrageously inefficient the following operation is1:

(car (cdr (filter prime? (enumerate-interval 10000 1000000))))

As far as I understand, stream are just a different, more efficient, implementation of sequences. This means we can get elegance without inefficiency. The trick is ``delayed evaluation''; a streams construct itself only when somebody needs it. Construction and use are interleaved. ``On the surface, streams are just lists with different names for the procedures that manipulate them.'' (319)

Exercise 3.50

Exercise:

Complete the following definition, which generalizes stream-map to allow procedures that take multiple arguments, analogous to map in section 2-2-3, footnote 12.

(define (stream-map proc . argstreams)
  (if (<??> (car argstreams))
      the-empty-stream
      (<??>
       (apply proc (map <??> argstreams))
       (apply stream-map
              (cons proc (map <??> argstreams))))))

Answer:

As far as I can see, Authors haven't shown the implementation of the map procedure presented in the footnote 12. Let's implement that first. I will call it map*.

(define (map* proc . args)
  (if (null? (car args))
      nil
      (cons
       (apply proc (map car args))
       (apply map* (cons proc (map cdr args))))))

Here is the stream version:

(define (stream-map proc . argstreams)
  (if (stream-null? (car argstreams))
      (the-empty-stream
       (cons
        (apply proc (map stream-car argstreams))
        (apply stream-map
               (cons proc (map stream-cdr argstreams)))))))

Exercise 3.51

Exercise:

In order to take a closer look at delayed evaluation, we will use the following procedure, which simply returns its argument after printing it:

(define (show x)
  (display-line x)
  x)

What does the interpreter print in response to evaluating each expression in the following sequence?(7)

(define x (stream-map show (stream-enumerate-interval 0 10)))

(stream-ref x 5)

(stream-ref x 7)

Answer:

;; relevant code
(define (stream-car stream) (car stream))
(define (stream-cdr stream) (force (cdr stream)))

(define (show x)
  (display-line x)
  x)

(define (display-line x)
  (newline)
  (display x))

(define (stream-map proc s)
  (if (stream-null? s)
      the-empty-stream
      (cons-stream (proc (stream-car s))
                   (stream-map proc (stream-cdr s)))))

(define (stream-enumerate-interval low high)
  (if (> low high)
      the-empty-stream
      (cons-stream low
                   (stream-enumerate-interval (+ low 1) high))))

(define (stream-ref s n)
  (if (= n 0)
      (stream-car s)
      (stream-ref (- n 1) (stream-cdr s))))

Executing (define x (stream-map show (stream-enumerate-interval 0 10))) only prints 0.

First, (stream-enumerate-interval 0 10) evaluates to a list (stream) whose car is 0 and whose cdr is a promise:

(0 . #<promise>)

Then we apply stream-map to show and to that list (stream) (0 . #<promise>).

stream-map evaluates to a list whose cdr is a promise; its car is (show (stream-car (0 . #<promise>))). This latter expression evaluates to 0, but it also has the side effect of printing a new line and 0.

So, when evaluating (define x (stream-map show (stream-enumerate-interval 0 10))), the interpreter prints 0.

When we evaluate (stream-ref x 5), stream-cdr is repeatedly called and the stream is consumed until the value 5 is found. Therefore, we print all the numbers from 1 to 5 (and the whole expression evaluates to 5).

Footnotes:

1

See p. 318 for another, less outrageous, example.

Send me an email for comments.

Created with Emacs 29.3.50 (Org mode 9.6.15)