3
votes

I have found some nice examples (here, here) of implementing SICP-like streams in Python. But I am still not sure how to handle an example like the integral found in SICP 3.5.3 "Streams as signals."

The Scheme code found there is

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

What is tricky about this one is that the returned stream int is defined in terms of itself (i.e., the stream int is used in the definition of the stream int).

I believe Python could have something similarly expressive and succinct... but not sure how. So my question is, what is an analogous stream-y construct in Python? (What I mean by a stream is the subject of 3.5 in SICP, but briefly, a construct (like a Python generator) that returns successive elements of a sequence of indefinite length, and can be combined and processed with operations such as add-streams and scale-stream that respect streams' lazy character.)

1

1 Answers

3
votes

There are two ways to read your question. The first is simply: How do you use Stream constructs, perhaps the ones from your second link, but with a recursive definition? That can be done, though it is a little clumsy in Python.

In Python you can represent looped data structures but not directly. You can't write:

l = [l]

but you can write:

l = [None]
l[0] = l

Similarly you can't write:

def integral(integrand,initial_value,dt):
    int_rec = cons_stream(initial_value,
                          add_streams(scale_stream(integrand,dt),
                                      int_rec))
    return int_rec

but you can write:

def integral(integrand,initial_value,dt):
    placeholder = Stream(initial_value,lambda : None)
    int_rec = cons_stream(initial_value,
                          add_streams(scale_stream(integrand,dt),
                                      placeholder))
    placeholder._compute_rest = lambda:int_rec
    return int_rec

Note that we need to clumsily pre-compute the first element of placeholder and then only fix up the recursion for the rest of the stream. But this does all work (alongside appropriate definitions of all the rest of the code - I'll stick it all at the bottom of this answer).

However, the second part of your question seems to be asking how to do this naturally in Python. You ask for an "analogous stream-y construct in Python". Clearly the answer to that is exactly the generator. The generator naturally provides the lazy evaluation of the stream concept. It differs by not being naturally expressed recursively but then Python does not support that as well as Scheme, as we will see.

In other words, the strict stream concept can be expressed in Python (as in the link and above) but the idiomatic way to do it is to use generators.

It is more or less possible to replicate the Scheme example by a kind of direct mechanical transformation of stream to generator (but avoiding the built-in int):

def integral_rec(integrand,initial_value,dt):
    def int_rec():
        for x in cons_stream(initial_value,
                    add_streams(scale_stream(integrand,dt),int_rec())):
            yield x
    for x in int_rec():
        yield x

def cons_stream(a,b):
    yield a
    for x in b:
        yield x

def add_streams(a,b):
    while True:
        yield next(a) + next(b)

def scale_stream(a,b):
    for x in a:
        yield x * b

The only tricky thing here is to realise that you need to eagerly call the recursive use of int_rec as an argument to add_streams. Calling it doesn't start it yielding values - it just creates the generator ready to yield them lazily when needed.

This works nicely for small integrands, though it's not very pythonic. The Scheme version works by optimising the tail recursion - the Python version will exceed the max stack depth if your integrand is too long. So this is not really appropriate in Python.

A direct and natural pythonic version would look something like this, I think:

def integral(integrand,initial_value,dt):
    value = initial_value
    yield value
    for x in integrand:
        value += dt * x
        yield value

This works efficiently and correctly treats the integrand lazily as a "stream". However, it uses iteration rather than recursion to unpack the integrand iterable, which is more the Python way.

In moving to natural Python I have also removed the stream combination functions - for example, replaced add_streams with +=. But we could still use them if we wanted a sort of halfway house version:

def accum(initial_value,a):
    value = initial_value
    yield value
    for x in a:
        value += x
        yield value

def integral_hybrid(integrand,initial_value,dt):
    for x in accum(initial_value,scale_stream(integrand,dt)):
        yield x

This hybrid version uses the stream combinations from the Scheme and avoids only the tail recursion. This is still pythonic and python includes various other nice ways to work with iterables in the itertools module. They all "respect streams' lazy character" as you ask.

Finally here is all the code for the first recursive stream example, much of it taken from the Berkeley reference:

class Stream(object):
        """A lazily computed recursive list."""
        def __init__(self, first, compute_rest, empty=False):
            self.first = first
            self._compute_rest = compute_rest
            self.empty = empty
            self._rest = None
            self._computed = False
        @property
        def rest(self):
            """Return the rest of the stream, computing it if necessary."""
            assert not self.empty, 'Empty streams have no rest.'
            if not self._computed:
                self._rest = self._compute_rest()
                self._computed = True
            return self._rest
        def __repr__(self):
            if self.empty:
                return '<empty stream>'
            return 'Stream({0}, <compute_rest>)'.format(repr(self.first))

Stream.empty = Stream(None, None, True)


def cons_stream(a,b):
    return Stream(a,lambda : b)

def add_streams(a,b):
    if a.empty or b.empty:
            return Stream.empty
    def compute_rest():
        return add_streams(a.rest,b.rest)
    return Stream(a.first+b.first,compute_rest)

def scale_stream(a,scale):
    if a.empty:
            return Stream.empty
    def compute_rest():
        return scale_stream(a.rest,scale)
    return Stream(a.first*scale,compute_rest)

def make_integer_stream(first=1):
      def compute_rest():
        return make_integer_stream(first+1)
      return Stream(first, compute_rest)

def truncate_stream(s, k):
        if s.empty or k == 0:
            return Stream.empty
        def compute_rest():
            return truncate_stream(s.rest, k-1)
        return Stream(s.first, compute_rest)

def stream_to_list(s):
        r = []
        while not s.empty:
            r.append(s.first)
            s = s.rest
        return r

def integral(integrand,initial_value,dt):
    placeholder = Stream(initial_value,lambda : None)
    int_rec = cons_stream(initial_value,
                          add_streams(scale_stream(integrand,dt),
                                      placeholder))
    placeholder._compute_rest = lambda:int_rec
    return int_rec

a = truncate_stream(make_integer_stream(),5)
print(stream_to_list(integral(a,8,.5)))