The Question Mark - blog by Mark Volkmann

Streams

Streams provide a way to iterate over many kinds of collections and resources, including String objects. They also provide a way to write data.

The class SequenceableCollection which is a superclass of Array provides the methods readStream, readStreamFrom:to:, and writeStream that each return a stream.

The readStream method of SequenceableCollection returns a ReadStream which is a subclass of Positionable stream. It has a position instance variable that can be retrieved by sending #position and modified by sending #position:. It also has a readLimit instance variable that is intialized to the collection size.

PositionableStream

The PositionableStream class method on: takes a Collection and returns a stream over it. The ReadStream class method on:from:to: takes a Collection and two indexes, and returns a stream over that range of elements.

The following table describes commonly used methods in the PositionableStream class.

MethodDescription
atEndanswers true if position >= readLimit
atStartanswers true if position is zero
backsubtract 1 from position and answers that element
contentsanswers copy of collection
isEmptyanswers true if atEnd and position is zero
next:anwsers collection of next argument elements, advancing position for each
notEmptyanswers true if not isEmpty
peekanswers element at position
peekBackanswers element at position minus 1
positionanswers the current value of position
position:sets position to argument if not greater than readLimit
resetsets position to zero
resetContentssets position and readLimit to zero
setToEndsets position to readLimit
skipadds 1 to position
skip:adds argument to position
skipBacksubtracts 1 from position
skipTo:advances position to element matching argument and answers whether found

ReadStream

The following table describes commonly used methods in the ReadStream class.

MethodDescription
nextadds 1 to position and answers that element
sizeanswers value of readLimit

The following code demonstrates using many of the methods described above. The logAs: method is defined in the “Getting Started” section “Transcripts”. When the end is reached, the next method returns nil.

arr := #('apple' 'banana' 'cherry').
stream := arr readStream.
stream position logAs: 'position'. "0"
stream size logAs: 'readLimit'. "3"
stream atStart logAs: 'atStart'. "true"
stream atEnd logAs: 'atEnd'. "false"
stream next print. "position -> 1; apple"
stream peek print. "position stays 1; banana"
stream peekBack print. "position stays 1; apple"
stream next print. "position -> 2; banana"
stream reset. "position -> 0"
stream next print. "position -> 1; apple"
stream position: 2.
stream next print. "position -> 3; cherry"
stream back. "position -> 2"
stream peekBack print. "banana"
stream peek print. "cherry"
stream atStart logAs: 'atStart'. "false"
stream atEnd logAs: 'atEnd'. "true"

The readStreamFrom:to: method returns a stream that can read the elements in a range of the collection.

arr := #('apple' 'banana' 'cherry' 'grape').
stream := arr readStreamFrom: 2 to: 3.
stream next print. "banana"
stream next print. "cherry"
stream next print. "nil"

WriteStream

The writeStream method returns a WriteStream that can modify the collection. It adds the instance variable writeLimit which is initialized to the collection size.

This class is often used to write to a characters in a string.

The following table describes some of the commonly instance methods defined in the WriteStream class that write to the stream.

MethodDescription
nextPut:writes a single Character
nextPutAll:writes all the Character elements in a String
newLinewrites a newline character
spacewrites a space character
tabwrites a tab character

Methods whose names begin with nextPut* write to the “next” position in the stream which is typically at the end. I kind of wish the method names were shortened to put*.

TODO: So far I can only get a WriteStream to modify existing elements, not add new ones. For example, this does not work:

coll := OrderedCollection newFrom: #('apple' 'banana' 'cherry').
stream := coll writeStream.
stream pastEndPut: 'grape'.
coll print.

ReadWriteStream

The ReadWriteStream class creates a stream that can read and write (modify) collection elements. Typically code only needs to read or write to a stream, not both, so this is rarely used.

coll := OrderedCollection newFrom: #('apple' 'banana' 'cherry').
stream := ReadWriteStream with: coll. "position is 3"
stream reset. "position is 0"
stream next print. "changes position to 1; outputs apple"
stream nextPut: 'grape'. "changes position to 2; changes element to grape"
coll print. "apple grape cherry"

Creating Strings

One use of streams is to efficiently build a String. For example:

stream := WriteStream on: (String new: 100).
stream nextPutAll: 'Hello'; nextPutAll: ', World'; nextPut: $!; newLine.
string := stream contents. "Hello, World!\n"

100 above is just a size estimate. It does not affect the size of the final String. More than 100 characters can be added, but doing that will be slightly less efficient because additional space will need to be allocated during execution of the nextPut* methods.

If the purpose of building a String is to write it to the Transcript, it is better to just use methods in the Transcript class. That class implements many of the same methods as the WriteStream class, but it is a subclass of Object, not any stream-related class. The Transcript stream-related methods are on the class side, whereas the corresponding WriteStream methods are on the instance side. The example above can be written as follows to write to the Transcript.

Transcript nextPutAll: 'Hello'; nextPutAll: ', World'; nextPut: $!; cr.

Debugging

When a stream variable is selected in the bottom panes of a debugger window, the current contents of the stream are displayed in the pane to its right. The current contents of a stream can also be displayed by selecting a stream variable and using “Inspect” or “Explore”.

File I/O

Files

To create a FileEntry object, send the #asFileEntry message to a string that contains the file name. The file is assumed to be in the \*-UserFiles directory (ex. Cuis-Smalltalk-Dev-UserFiles). To see this, enter DirectoryEntry currentDirectory in a Workspace and “Print it”.

For example:

fileEntry := 'demo.txt' asFileEntry.

To write to a text file, overwriting any previous contents:

fileEntry forceWriteStreamDo: [:fileStream |
    fileStream nextPutAll: 'line #1'.
    fileStream newLine.
    fileStream nextPutAll: 'line #2'
].

Another way to obtain a stream for writing and reading a file at a given path is the following:

stream := FileStream fileNamed: 'some-file-path'

To read the entire contents of a text file into a string:

contents := fileEntry fileContents.

To read lines in a text file one at a time:

fileEntry := 'demo.txt' asFileEntry.
stream := fileEntry readStream.
line := stream nextLine "do repeatedly to get each line"

To write serialized objects to a file:

fileEntry writeStreamDo: [:fileStream |
    | refStream |
    refStream := ReferenceStream on: fileStream.
    refStream nextPut: true.
    refStream nextPut: 3.
    refStream nextPut: 'text'.
    refStream nextPut: #(1 2 3).
].

TODO: Do ReferenceStreams support circular object references?

To read serialized objects from a file:

fileEntry readStreamDo: [:fileStream |
    | object refStream |
    refStream := ReferenceStream on: fileStream.
    [refStream atEnd] whileFalse: [
        object := refStream next.
        object print "writes to Transcript"
    ]
]

To delete a file:

fileEntry delete.

TODO: Harvest more information about file system operations from this video. squeak smalltalk tutorial: file handling part 1.

Directories

The following instance methods of the DirectoryEntry class iterate over its files and subdirectories:

| Method | Description | | ------------------- | --------------------------------------------------------------------------- | --- | | allChildrenDo: | iterates over all subdirectories and files recursively | | | allDirectoriesDo: | iterates over all subdirectories recursively | | allFilesDo: | iterates over all files in this directory and in subdirectories recursively | | directoriesDo: | iteraters over all subdirectories non-recursively | | filesDo: | iterates over all files in this directory | | childrenDo: | iterates over all subdirectories and files non-recursively | |

The following code iterates over all the files found in and below a given directory:

de := DirectoryEntry smalltalkImageDirectory.
de allFilesDo: [:file | file name print]