Chapter 8: Streams


Return to [ Table of Contents] [ Main Page] [ Previous Chapter] [ Other Internet Smalltalk Resources]

What are Streams?

Streams are objects that support sequential access to collections and files of sequential data. Streams can be open on sequenceable collections of any kind of object, on a source of random numbers, or on files.

There are two separate sets of stream classes, one for streams on collections, and one for streams on files.

Streams on collections are instances of ReadStream, WriteStream, or ReadWriteStream, which are subclasses of PositionableStream, which is a subclass of Stream. The other subclass of Stream is EsRandom.

Streams on files are instances of ReadFileStream, WriteFileStream, or ReadWriteFileStream, which are subclasses of FileStream.

The two stream hierarchies are not physically related, as was shown in the table of contents for this chapter, but they work on the same principles and respond to almost the same set of messages, differing only in the kinds of objects that can be used, and differences due to the objects, files, or collections, over which the stream operates.

All streams respond to three messages.

       atEnd              Has the final item been consumed? 
       do:                Iterate through a stream
       next               Answer the next item in the stream
 
These are the only messages that random number streams answer to.

Streams on Collections

Streams are devices that provide sequential access to collections. While they are usually used on strings, they work regardless of the kind of objects that the collection contains. They are particularly useful when multiple items (elements) need to be fetched or added to a collection. They also support methods such as iterating with do: and checking for the end of the stream.

Streams provide an abstraction that enables store and retrieve operations to be performed on the elements of an underlying composite data structure, usually an indexed collection. In particular, a stream remembers the position of the last element in the data structure to be read or written. Streams are positionable, that is, the current read/write position can be changed using the stream's public interface. This allows elements to be accessed in either a sequential or non-sequential fashion.

It is implicit in stream semantics that the elements of the underlying data structure are sequentially ordered. With respect to the Common Language Data Type classes, a ReadWriteStream or WriteStream can stream over an Array, a String, or a DBString(characters in the range 0 to 65535, that is, double-byte (DB) charaters). A ReadStream can stream over the same set of classes plus OrderedCollection, SortedCollection, or Symbol.

Positionable Streams

Streams on collections are all subclasses of the abstract class PositionableStream which provides a common protocol. Class PositionableStream defines methods for read and write streams. The streams can be on any sequenceable collection. Operations include retrieving the contents (the possibly modified original collection), setting and retrieving the position, and skipping elements.

Positionable streams have a collection across which they stream, starting with the first element and continuing to the last, unless explicitly changed. Streams keep an internal position for the previous element. Positionable streams can be opened on any kind of indexed collection.

      atEnd              Has the final item been consumed?   
      close              Close the stream 
      contents           Answer the whole contents of the stream
      isEmpty            Is the stream empty? 
      position           Get the current position of the stream 
      position: n        Set the current position of the stream 
      reset              Set the position to the front of the collection 
      setToEnd           Set the position to the end of the collection
      size               Answer size of file in bytes
      skip: n            Skip the next n elements 
      upToEnd            Answer all items up to the stream's end
 

Read Streams

Read streams are instances of class ReadStream. Class ReadStream streams across any sequenceable collection. The collection is not modifiable. Operations include reading from the stream and skipping ahead to a specified sequence. They are created by sending the on: message to the class with some sequenceable collection.


  ReadStream on: 'Mary had a little laptop, its screen was white as snow' 
 
Items are taken (read) from the stream with these messages:
       
      copyFrom:to:              Copy into new collection
      do:                       Iterate through a stream; never terminates
      next                      Answer the next element
      next: n                   Answer the next n elements as a collection
      nextLine                  Answer up to the next line delimiter  
      nextMatchFor: anObjec     Answer all elements up to anObject  
      peek                      Peek at next element 
      peekFor:                  Compare next element with an object 
      skip: n                   Skip n items 
      skipTo: item              Skip items to the next occurrence of item 
      skipToAll: aCollection    Skip items to aCollection 
      upTo: item                Return items up to next occurrence of item 
      upToAll: aCollection      Skip items up to aCollection  
      upToEnd                   Get elements up to the end 
 

The next message is used when the stream needs to be processed one item at a time.

- To get the next character from a read stream:

  | rs | 
  rs := ReadStream on: 'Mary had a little laptop, its screen was white as
snow'. 
  ^rs next 
    Answers: $M 
 
- To get the next word from a read stream:
  | rs |  
  rs := ReadStream on: 'Mary had a little laptop, its screen was white as snow'.
 
  ^rs upTo: $                "Get up to the next blank"  
    Answers: 'Mary'  
 
- To get the next line from a read stream:
  | rs |  
  rs := ReadStream on: 'Mary had a little laptop,',LineDelimiter,'its screen was white as snow'.
 
  ^rs nextLine  
    Answers: 'Mary had a little laptop,'  
 

Write Streams

Write streams are instances of class WriteStream. Class WriteStream allows writing to a sequenceable collection, but not reading from it. Setting the write position truncates items past that point. Write streams are frequently used to construct strings that will be displayed or printed. They are created by sending the on: message to the class, passing a fixed-size sequenceable collection such as a String, Array, or ByteArray.

      WriteStream on: String new  
 
or:
      WriteStream on: Array new  
 
The output collection will be automatically extended when it gets full.

Items are written to the stream with messages such as:

      flush                        Flush buffers to disk 
      nextPut: item                Put a single item  
      nextPutAll: collection       Put a collection  
      next: n put: item            Put n copies of item  
      cr                           Put a line delimiter  
      space                        Put a space  
      tab                          Put a tab  
 

The next example writes two lines to a stream, and then uses the contents message to retrieve a copy of the elements written to the stream.

- Simple Write Stream

  | ws |  
  ws := WriteStream on: String new.  
  ws nextPutAll: 'Mary had a little laptop,'; cr.  
  ws nextPutAll: 'it''s screen was white as snow;'; cr.  
  ws nextPutAll: 'and everywhere that Mary went;'; cr.  
  ws nextPutAll: 'her laptop was sure to go.'; cr.  
  ^ ws contents  
        - Answers:  
              'Mary had a little laptop,  
              it's screen was white as snow;  
              and everywhere that Mary went,  
              her laptop was sure to go.'  
 

Read and Write Streams

Class ReadWriteStream allows both reading and writing a stream. A ReadWriteStream combines the above protocols and adds messages for truncating and setting the position in the stream. Thus, random access is allowed.

ReadWriteStream answers to the same messages as ReadStream and WriteStream, as well as the truncating message:

       
      copyFrom:to:               Copy into new collection 
      cr                         Put a line delimiter 
      do:                        Iterate through a stream; never terminates 
      flush                      Flush buffers to disk 
      next                       Answer the next element  
      next: n                    Answer the next n elements as a collection
      nextLine                   Answer up to the next line delimiter  
      nextMatchFor: anObject     Answer all elements up to anObject  
      nextPut: item              Put a single item
      nextPutAll: collection     Put a collection  
      next: n put: item          Put n copies of item  
      peek                       Peek at next element 
      peekFor:                   Compare next element with an object 
      skip: n                    Skip n items 
      skipTo: item               Skip items to the next occurrence of item 
      skipToAll: aCollection     Skip items to aCollection 
      space                      Put a space  
      tab                        Put a tab  
      truncate                   truncate objects? 
      upTo: item                 Return items up to next occurrence of item 
      upToAll: aCollection       Skip items up to aCollection  
      upToEnd                    Get elements up to the end 
 

Random Streams

Instances of class EsRandom produce a stream of machine-generated pseudo-random floating-point numbers of good quality in the range 0.0 through, but not including, 1.0. The random number generator is self-seeding, using the system clock; it is not possible to override the self-seeding process. Each instance of EsRandom is seeded differently, and thus answer a different sequence of random numbers.

Random streams answer to these messages:

    atEnd           Always answers false  
    do:             Iterate through a stream; never terminates  
    next            Answer the next random number  
 
As a Random Stream example, shuffle a 'deck' of 'cards' using random numbers. The cards are represented by integers from 1 to 52 in an ordered collection; removal is random. Removed cards are placed into a second array.

Shuffle a deck of cards 
  | deck rand shuf n |  
  rand := EsRandom new.                      Get new generator.  
  deck := (1 to: 52) as OrderedCollection.   Init deck to integers 1 to 52
 
  shuf := OrderedCollection new: deck size.  Put shuffled cards here  
  deck size timesRepeat: [                   Loop on initial deck size  
    n := (deck size * rand next)  
      floor asInteger + 1.                   Rand range 1 to cur deck size 
    shuf addLast: (deck removeAtIndex: n) ]. Move card to shuffled deck  
  shuf size = 52 ifFalse:  
    [self error: 'This better not ever happen!' ].  
  ^shuf  
 

Line Delimiters

By default, a stream uses the platform file system's line delimiter for operations such as cr and nextLine. Line delimiters are output by the cr message and used when reading to delimit input lines. The line delimiter defaults to the one that is used by the native operating system of the platform. Line delimiters are used in both streams and file streams.

A string representing the current line delimiter can be obtained by sending the lineDelimiter message to a file stream instance, and can be changed to an arbitrary string by sending the lineDelimiter: message. This makes it possible to adopt a single platform's file convention as the standard for all platforms, or to use nextLine to read files written on other platforms.

The line delimiters can be changed in various ways to match other supported platforms (by name) or to other sequences. The line delimiters are in the pool dictionary CldtConstants. The default is named LineDelimiter.

Line delimiters are set with the lineDelimiter: message to a stream.

The two examples below are the same, provided that the default line delimiter has not been changed.

  aStream cr.                           "Output a line delimiter" 
  aStream nextPutAll: LineDelimiter     "Output a line delimiter"  
 

The next example demonstrates the use of the lineDelimiter: message, as well as their effect on the cr message:

To set to the default line delimiter:  
  aStream lineDelimiter: LineDelimiter. 
  cr;  nextPutAll:  '<-default line delimiter'. 
 

File Streams

File streams are very similar to streams over collections, but operate upon files in the platform file system. File streams read and write characters or bytes only. The naming of files and directories is constrained by the platform operating system.

There are three kinds of file streams:

    ReadFileStream         Read files only 
    WriteFileStream        Write files only  
    ReadWriteFileStream    Reads and write files  
 

Opening and Closing

The simplest means of opening a new file stream instance on a particular file is with the open: or openEmpty: messages, and closing is done with the close message.

The open: message opens an existing file, while the openEmpty: message truncates an existing file (to size 0) or creates the file if it does not exist.

Generally, open: is used when reading files and openEmpty: is used when writing new files or overwriting existing files.

Here is an example that illustrates how to open an existing file for reading and properly check for open errors.

Open existing file for reading; check for open errors 
  | file |  
  (file := ReadFileStream open: 'existing.txt') isError  
    ifTrue: [ ^self error: file message ].  
  "...use the file stream for reading..." 
  file close.  "when done, close the file stream." 
 
Here is an example that illustrates how to open an existing file for reading and writing and properly check for open errors.

Open existing file for reading and writing, or creates the file  
  if it doesn't exist; check for open errors.   
  | file |  
  (file := ReadWriteFileStream open: 'existing.txt') isError  
    ifTrue: [ ^self error: file message ].  
  "...use the file stream for reading and/or writing..." 
  file close.  
 
Here is an example that illustrates how to open an existing file for writing only, using the openEmpty: message, and properly check for open errors.

Open file for writing (empty it if it exists); check for open
errors. 
 
  | file |  
  (file := WriteFileStream openEmpty: 'new.txt') isError  
    ifTrue: [ ^self error: file message ].  
  "...use the file stream for writing..." 
  file close  
 
Once a file stream is open, it is operated upon with the same set of messages used for streams, including, for reading:

    next                    Answer the next element  
    next: n                 Answer the next n elements as a collection   
    nextLine                Answer up to the next line delimiter   
    nextMatchFor: anObject  Answer all elements up to anObject   
    skip: n                 Skip n items   
    skipTo: item            Skip items to the next occurrence of item   
    skipToAll: aCollection  Skip items to aCollection   
    upTo: item              Return items up to next occurrences of item   
    upToAll: aCollection    Skip items up to aCollection   
 
and for writing:

    nextPut: item           Put a single item   
    nextPutAll: collection  Put a collection  
    next: n put: item       Put n copies of item  
    cr                      Put a line delimiter   
    space                   Put a space   
    tab                     Put a tab   
 
Once all desired operations have been performed, the file stream instance must be closed by sending it the close message before it is discarded. This closes the file, by deallocating any operating system resources associated with the file stream, and flushing any cached data to disk.

On double-byte platforms, the platform character encoding does not necessarily match the character encoding used within Smalltalk. As a result, Smalltalk strings must be converted to and from the platform representation as they are written to and read from files. When the Smalltalk and platform encodings differ, the stream answered by the open: and openEmpty: messages will not be an instance of the class to which the message was sent. In such cases the open: and openEmpty: messages answer a specialized stream that conforms to the requested protocols and manages the conversion of Smalltalk strings to the appropriate platform representation.

In these cases, it is important to use the isError message to test the result of the open: and openEmpty: operations rather than testing the class of the returned object.

Reading and Writing

The following example is and earlier example modified to write to a file stream.

Simple Write File Stream  
  | ws text |   
  (ws := ReadWriteFileStream openEmpty: 'maryslap.txt') isError  
    ifTrue: [ ^self error: ws message ].   
  ws nextPutAll: 'Mary had a little lamp,'; cr.   
  ws nextPutAll: 'it''s light was white as show;'; cr.   
  ws nextPutAll: 'and everywhere that Mary went;'; cr.   
  ws nextPutAll: 'that lamp was sure to glow.'; cr.   
  text := ws contents.   
  ws close.   
  ^ text   
       -Answers:   
            'Mary had a little lamp,   
            it's light was white as snow;   
            and everywhere that Mary went,   
            that lamp was sure to glow.'   
 
The next example shows how to open an existing file for reading, another for writing, and then write the input file's entire contents to the output file. After two opens, the expression old contents reads the whole contents of the open file old into memory; then nextPutAll: outputs it all to new.

Copy a file all at once; prompt for file names  
  | old new |   
  (old := ReadFileStream   
    open: (CxFileSelectionPrompter new prompt)) isError   
      ifTrue: [ ^self error: old message ].   
  (new := WriteFileStream   
    openEmpty: (System prompt:'Output file name')) isError   
      ifTrue: [^self error: new message ].   
  new nextPutAll: old contents.   
  old close.   
  new close.   
 
The next example reads a text file and changes the first lower case character which follows a period, possibly with separating white space, into an upper case character. It will take text such as:

  the dog ran up the tree.  the cat barked.  the boy laughed.  
 
and convert it to:

  The dog ran up the tree.  The cat barked.  The boy laughed.  
 
Capitalize first letter past a period in a file.    
  | input output str period |  
  period := true.   
 
  str := CwFileSelectionPrompter new title: 'Select input file';  
  (input := ReadFileStream open: str) isError   
    ifTrue: [ ^self error: input message ].   
 
  str := System prompt: 'Enter name of output file'.   
  (output := WriteFileStream open: str) isError   
    ifTrue: [ ^self error: output message ].   
 
  [input atEnd ]   
    whileFalse: [ |char|    
      char := input next.   
      period & char isLowercase   
        ifTure: [ output nextPut: char asUppercase ]   
        ifFalse: [output nextPut: char ].   
      char isSeparator  
        ifFalse: [ period := char == $.] ].  
  input close.   
  output close.   
 


Summary

Let's review some of the important protocols:


Return to [Top of the page]
Smalltalk Tutorial

Go to Glossary

Return to Chapter7: Collections

Return to Main Page