5.10 Smalltalk Streams
Our examples have used a mechanism extensively, even
though we haven't discussed it yet. The Stream class provides
a framework for a number of data structures, including
input and output functionality, queues, and endless sources
of dynamically-generated data. A Smalltalk stream is quite
similar to the UNIX streams you've used from C. A stream
provides a sequential view to an underlying resource; as you
read or write elements, the stream position advances until
you finally reach the end of the underlying medium. Most
streams also allow you to set the current position, providing
random access to the medium.
5.10.1 The Output Stream
The examples in this book all work because they write
their output to the Transcript stream. Each class implements
the printOn: method, and writes its output to the supplied
stream. The printNl method all objects use is simply to
send the current object a printOn: message whose argument is
Transcript (by default attached to the standard output stream
found in the stdout global). You can invoke the standard output stream
directly:
| 'Hello, world' printOn: stdout !
stdout inspect !
|
or you can do the same for the Transcript, which is yet another stream:
| 'Hello, world' printOn: stdout !
Transcript inspect !
|
the last inspect statement will show you how the Transcript is
linked to stdout (32).
5.10.2 Your Own Stream
Unlike a pipe you might create in C, the underlying
storage of a Stream is under your control. Thus, a Stream
can provide an anonymous buffer of data, but it can also
provide a stream-like interpretation to an existing array of
data. Consider this example:
| Smalltalk at: #a put: (Array new: 10) !
a at: 4 put: 1234 !
a at: 9 put: 5678 !
Smalltalk at: #s put: (ReadWriteStream on: a) !
s inspect !
s position: 1 !
s inspect !
s nextPut: 11; nextPut: 22 !
(a at: 1) printNl !
a do: [:x| x printNl] !
s position: 2 !
s do: [:x| x printNl] !
s position: 5 !
s do: [:x| x printNl] !
s inspect !
|
The key is the on: message; it tells a stream class to
create itself in terms of the existing storage. Because of
polymorphism, the object specified by on: does not have to
be an Array; any object which responds to numeric at: messages
can be used. If you happen to have the NiledArray
class still loaded from the previous chapter, you might try
streaming over that kind of array instead.
You're wondering if you're stuck with having to know
how much data will be queued in a Stream at the time you
create the stream. If you use the right class of stream,
the answer is no. A ReadStream provides read-only access to
an existing collection. You will receive an error if you
try to write to it. If you try to read off the end of the
stream, you will also get an error.
By contrast, WriteStream and ReadWriteStream (used in
our example) will tell the underlying collection to grow
when you write off the end of the existing collection. Thus,
if you want to write several strings, and don't want to add up their
lengths yourself:
| Smalltalk at: #s put: (ReadWriteStream on: (String new)) !
s inspect !
s nextPutAll: 'Hello, '!
s inspect !
s nextPutAll: 'world'!
s inspect !
s position: 1 !
s inspect !
s do: [:c | stdout nextPut: c ] !
(s contents) printNl !
|
In this case, we have used a String as the collection
for the Stream. The printOn: messages add bytes to the initially
empty string. Once we've added the data, you can
continue to treat the data as a stream. Alternatively, you
can ask the stream to return to you the underlying object.
After that, you can use the object (a String, in this example)
using its own access methods.
There are many amenities available on a stream object.
You can ask if there's more to read with atEnd . You can
query the position with position , and set it with position: .
You can see what will be read next with peek , and
you can read the next element with next .
In the writing direction, you can write an element with
nextPut: . You don't need to worry about objects doing a
printOn: with your stream as a destination; this operation
ends up as a sequence of nextPut: operations to your stream.
If you have a collection of things to write, you can use
nextPutAll: with the collection as an argument; each member
of the collection will be written onto the stream. If you
want to write an object to the stream several times, you
can use next:put: , like this:
| Smalltalk at: #s put: (ReadWriteStream on: (Array new: 0)) !
s next: 4 put: 'Hi!' !
s position: 1 !
s do: [:x | x printNl] !
|
5.10.3 Files
Streams can also operate on files. If you wanted to
dump the file `/etc/passwd' to your terminal, you could
create a stream on the file, and then stream over its contents:
| Smalltalk at: #f put: (FileStream
open: '/etc/passwd'
mode: FileStream read) !
f do: [ :c | Transcript nextPut: c ] !
f position: 30 !
25 timesRepeat: [ Transcript nextPut: (f next) ] !
f close !
|
and, of course, you can load Smalltalk source code into your
image:
| FileStream fileIn: '/users/myself/src/source.st' !
|
5.10.4 Dynamic Strings
Streams provide a powerful abstraction for a number of
data structures. Concepts like current position, writing
the next position, and changing the way you view a data
structure when convenient combine to let you write compact,
powerful code. The last example is taken from the actual
Smalltalk source code--it shows a general method for making
an object print itself onto a string.
| printString
| stream |
stream := WriteStream on: (String new).
self printOn: stream.
^stream contents
!
|
This method, residing in Object, is inherited by every
class in Smalltalk. The first line creates a WriteStream
which stores on a String whose length is currently 0
(String new simply creates an empty string. It
then invokes the current object with printOn: . As the
object prints itself to "stream", the String grows to accommodate
new characters. When the object is done printing,
the method simply returns the underlying string.
As we've written code, the assumption has been that
printOn: would go to the terminal. But replacing a stream
to a file like `/dev/tty' with a stream to a data
structure (String new ) works just as well. The last line
tells the Stream to return its underlying collection, which will
be the string which has had all the printing added to it. The
result is that the printString message returns an object of
the String class whose contents are the printed representation
of the very object receiving the message.
|