Streams
Streams (also called delayed Lists or lazy lists) are a data structure described in SICP Chapter 3.5. It also appears in Chapter 21 of Functional Programming in Scheme.
The base of those data structures are two expressions delay
and force
.
The result of a delay is often called a Promise.
To create a lazy pair, you use cons with the first element
(car
) and the rest (cdr
) is a delay expression:
(define s (cons 1 (delay 2)))
If you print this expression, you will get something like this (it depends on Scheme implementation)
(write (cons 1 (delay 2)))
;; ==> (1 . #<promise - not forced>)
The cdr
is a promise that needs to be forced to get evaluated.
(let ((x (cons 1 (delay 2))))
(write (force (cdr x)))
(newline)
(write x)
(newline))
;; ==> 2
;; ==> (1 . #<promise - forced with number>)
With this you can create infinite lists because the delay can point into the same object.
Let's define some helper procedures:
(define-macro (stream-cons x y)
`(cons ,x (delay ,y)))
This is lazy version of cons that utilize lisp macro.
You can also define car
and cdr
versions that work with streams:
(define (stream-car stream)
(car stream))
(define (stream-cdr stream)
(force (cdr stream)))
stream-car is the same as car so you can use an alias:
(define stream-car car)
We also need an empty stream and predicate that check if stream is empty:
(define (empty-stream? x) (eq? x the-empty-stream))
(define the-empty-stream '())
To create an infinite stream of ones, you can use code like this:
(define ones (stream-cons 1 ones))
(write (stream-car (stream-cdr (stream-cdr ones))))
;; ==> 1
Let's write a procedure that take a stream and return a list of first n elements.
(define (stream-take n stream)
(if (<= n 0)
'()
(cons (stream-car stream) (stream-take (- n 1) (stream-cdr stream)))))
(write (stream-take 10 ones))
;; ==> (1 1 1 1 1 1 1 1 1 1)
You can define procedures that operate on streams, like procedure that add two streams:
(define (stream-add s1 s2)
(let ((first-a (stream-car s1))
(first-b (stream-car s2)))
(stream-cons
(+ first-a first-b)
(add-streams (stream-cdr s1) (stream-cdr s2)))))
This function don't check if any of the streams are not empty. So it will only work on infnite streams.
You can use this function to create stream of integers:
(define integers (stream-cons 1 (stream-add integers ones)))
To prove that it works, you can get first 10 elements with stream-take
:
(display (stream-take 10 integers))
;; ==> (1 2 3 4 5 6 7 8 9 10)
We can also define higher order procedures that operate on streams. They will not execute until you
use force
. Example cam be stream-map
:
(define (stream-map proc . streams)
(define (single-map proc stream)
(if (empty-stream? stream)
the-empty-stream
(stream-cons (apply proc (stream-car stream))
(single-map proc (stream-cdr stream)))))
(single-map proc (apply stream-zip streams)))
It requires helper procedure that combine two streams by taking each element from every stream and creates a single list in output stream.
(define (stream-zip . streams)
(if (or (null? streams) (some empty-stream? streams))
the-empty-stream
(stream-cons (apply list (map stream-car streams))
(apply stream-zip (map stream-cdr streams)))))
(define (some fn lists)
(if (or (null? lists) (some/1 null? lists))
#f
(or (apply fn (map car lists))
(some fn (map cdr lists)))))
(define (some/1 fn lst)
(if (fn lst)
#f
(or (fn (car lst))
(some/1 (cdr lst)))))
The function work like this:
(stream-take 5 (stream-zip integers integers ones))
;; ==> ((1 1 1) (2 2 1) (3 3 1) (4 4 1) (5 5 1))
With map, you can simplify stream-add
with stream-map
:
(define (streams-add s1 s2)
(stream-map + s1 s2))
Another useful procedures that accept stream are stream-limit
and force-stream
:
(define (stream-limit n stream)
(let loop ((n n) (stream stream))
(if (or (empty-stream? stream) (= n 0))
the-empty-stream
(stream-cons (stream-car stream)
(loop (- n 1)
(stream-cdr stream))))))
(define (stream-force stream)
(let loop ((stream stream))
(if (empty-stream? stream)
'()
(cons (stream-car stream)
(loop (stream-cdr stream))))))
If you call force-stream
on infinite stream it will create infinite loop, but note that the
procedure force-stream is not tail recursive. The recursive call to named let
is not the last
expression. The last expression is cons
, so it will throw stack overflow error.
You can try to create a tail recursive version of the procedure as an exercise.
If you combine both procedures, you can create the same effect as with stream-take
:
(stream-force (stream-limit 10 integers))
;; ==> (1 2 3 4 5 6 7 8 9 10)
So you can implement stream-take
with above procedures:
(define (stream-take n stream)
(stream-force (stream-limit n stream)))
(stream-take 10 integers)
;; ==> (1 2 3 4 5 6 7 8 9 10)
Another useful procedure is stream-reduce
:
(define (stream-reduce fun stream)
(let loop ((result (stream-car stream))
(stream (stream-cdr stream)))
(if (empty-stream? stream)
result
(loop (fun result (stream-car stream))
(stream-cdr stream)))))
You can implement factorial function using streams:
(define (! n)
(stream-reduce * (stream-limit n integers)))
(! 10)
;; ==> 3628800
and you can use this procedure to create a stream of factorials:
(define factorials
(stream-map ! integers))
(stream-take 10 factorials)
;; ==> (1 2 6 24 120 720 5040 40320 362880 3628800)
Another example of using streams is calculation of Fibonacci numbers:
(define fibs
(cons-stream 0
(cons-stream 1
(add-streams (stream-cdr fibs) fibs))))
(stream-take 10 fibs)
;; ==> (0 1 1 2 3 5 8 13 21 34)