Skip to main content
Engineering LibreTexts

12.3: Streaming over Collections

  • Page ID
    39642
    \( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \) \( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)\(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\) \(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\)\(\newcommand{\AA}{\unicode[.8,0]{x212B}}\)

    Streams are really useful when dealing with collections of elements, and can be used for reading and writing those elements. We will now explore the stream features for collections.

    Reading collections

    Using a stream to read a collection essentially provides you a pointer into the collection. That pointer will move forward on reading, and you can place it wherever you want. The class ReadStream should be used to read elements from collections.

    Messages next and next: defined in ReadStream are used to retrieve one or more elements from the collection.

    | stream |
    stream := ReadStream on: #(1 (a b c) false).
    stream next.
    >>> 1
    stream next.
    >>> #(#a #b #c)
    stream next.
    >>> false
    
    | stream |
    stream := ReadStream on: 'abcdef'.
    stream next: 0.
    >>> ''
    stream next: 1.
    >>> 'a'
    stream next: 3.
    >>> 'bcd'
    stream next: 2.
    >>> 'ef'
    

    The message peek defined in PositionableStream is used when you want to know what is the next element in the stream without going forward.

    stream := ReadStream on: '-143'.
    "look at the first element without consuming it."
    negative := (stream peek = $-).
    negative.
    >>> true
    "ignores the minus character"
    negative ifTrue: [ stream next ].
    number := stream upToEnd.
    number.
    >>> '143'
    

    This code sets the boolean variable negative according to the sign of the number in the stream, and number to its absolute value. The message upToEnd defined in ReadStream returns everything from the current position to the end of the stream and sets the stream to its end. This code can be simplified using the message peekFor: defined in PositionableStream, which moves forward if the following element equals the parameter and doesn’t move otherwise.

    | stream |
    stream := '-143' readStream.
    (stream peekFor: $-)
    >>> true
    stream upToEnd
    >>> '143'
    

    peekFor: also returns a boolean indicating if the parameter equals the element.

    You might have noticed a new way of constructing a stream in the above example: one can simply send the message readStream to a sequenceable collection (such as a String) to get a reading stream on that particular collection.

    Positioning

    There are messages to position the stream pointer. If you have the index, you can go directly to it using position: defined in PositionableStream. You can request the current position using position. Please remember that a stream is not positioned on an element, but between two elements. The index corresponding to the beginning of the stream is 0.

    A stream at position 2.
    Figure \(\PageIndex{1}\): A stream at position 2.

    You can obtain the state of the stream depicted in \(\PageIndex{1}\) with the following code:

    | stream |
    stream := 'abcde' readStream.
    stream position: 2.
    stream peek
    >>> $c
    

    To position the stream at the beginning or the end, you can use the message reset or setToEnd. The messages skip: and skipTo: are used to go forward to a location relative to the current position: skip: accepts a number as argument and skips that number of elements whereas skipTo: skips all elements in the stream until it finds an element equal to its parameter. Note that it positions the stream after the matched element.

    | stream |
    stream := 'abcdef' readStream.
    stream next.
    >>> $a "stream is now positioned just after the a"
    stream skip: 3. "stream is now after the d"
    stream position.
    >>> 4
    stream skip: -2. "stream is after the b"
    stream position.
    >>> 2
    stream reset.
    stream position.
    >>> 0
    stream skipTo: $e. "stream is just after the e now"
    stream next.
    >>> $f
    stream contents.
    >>> 'abcdef'
    

    As you can see, the letter e has been skipped.

    The message contents always returns a copy of the entire stream.

    Testing

    Some messages allow you to test the state of the current stream: atEnd returns true if and only if no more elements can be read, whereas isEmpty returns true if and only if there are no elements at all in the collection.

    Here is a possible implementation of an algorithm using atEnd that takes two sorted collections as parameters and merges those collections into another sorted collection:

    | stream1 stream2 result |
    stream1 := #(1 4 9 11 12 13) readStream.
    stream2 := #(1 2 3 4 5 10 13 14 15) readStream.
    
    "The variable result will contain the sorted collection."
    result := OrderedCollection new.
    [stream1 atEnd not & stream2 atEnd not ]
        whileTrue: [
            stream1 peek < stream2 peek
            "Remove the smallest element from either stream and add it to
                the result."
            ifTrue: [result add: stream1 next ]
            ifFalse: [result add: stream2 next ] ].
            
    "One of the two streams might not be at its end. Copy whatever remains."
    result
        addAll: stream1 upToEnd;
        addAll: stream2 upToEnd.
    
    result.
    >>> an OrderedCollection(1 1 2 3 4 4 5 9 10 11 12 13 13 14 15)
    

    Writing to collections

    We have already seen how to read a collection by iterating over its elements using a ReadStream. We’ll now learn how to create collections using WriteStreams.

    WriteStreams are useful for appending a lot of data to a collection at various locations. They are often used to construct strings that are based on static and dynamic parts, as in this example:

    | stream |
    stream := String new writeStream.
    stream
        nextPutAll: 'This Smalltalk image contains: ';
        print: Smalltalk allClasses size;
        nextPutAll: ' classes.';
        cr;
        nextPutAll: 'This is really a lot.'.
    stream contents.
    >>> 'This Smalltalk image contains: 2322 classes.
    This is really a lot.'
    

    This technique is used in the different implementations of the method printOn:, for example. There is a simpler and more efficient way of creating strings if you are only interested in the content of the stream:

    | string |
    string := String streamContents:
        [ :stream |
            stream
                print: #(1 2 3);
                space;
                nextPutAll: 'size';
                space;
                nextPut: $=;
                space;
                print: 3. ].
    string.
    >>> '#(1 2 3) size = 3'
    

    The message streamContents: defined SequenceableCollection creates a collection and a stream on that collection for you. It then executes the block you gave passing the stream as a parameter. When the block ends, streamContents: returns the contents of the collection.

    The following WriteStream methods are especially useful in this context:

    nextPut: adds the parameter to the stream;

    nextPutAll: adds each element of the collection, passed as a parameter, to the stream;

    print: adds the textual representation of the parameter to the stream.

    There are also convenient messages for printing useful characters to a stream, such as space, tab and cr (carriage return). Another useful method is ensureASpace which ensures that the last character in the stream is a space; if the last character isn’t a space it adds one.

    About String Concatenation

    Using nextPut: and nextPutAll: on a WriteStream is often the best way to concatenate characters. Using the comma concatenation operator (,) is far less efficient:

    [| temp |
        temp := String new.
        (1 to: 100000)
            do: [:i | temp := temp, i asString, ' ' ] ] timeToRun
    >>> 115176 "(milliseconds)"
    
    [| temp |
        temp := WriteStream on: String new.
        (1 to: 100000)
            do: [:i | temp nextPutAll: i asString; space ].
        temp contents ] timeToRun
    >>> 1262 "(milliseconds)"
    

    The reason that using a stream can be much more efficient is that using a comma creates a new string containing the concatenation of the receiver and the argument, so it must copy both of them. When you repeatedly concatenate onto the same receiver, it gets longer and longer each time, so that the number of characters that must be copied goes up exponentially. This also creates a lot of garbage, which must be collected. Using a stream instead of string concatenation is a well-known optimization.

    In fact, you can use the message streamContents: defined in SequenceableCollection class (mentioned earlier) to help you do this:

    String streamContents: [ :tempStream |
        (1 to: 100000)
            do: [:i | tempStream nextPutAll: i asString; space ] ]
    

    Reading and writing at the same time

    It’s possible to use a stream to access a collection for reading and writing at the same time. Imagine you want to create a History class which will manage backward and forward buttons in a web browser. A history would react as in Figures \(\PageIndex{2}\) to \(\PageIndex{8}\).

    History stream progression 1.
    Figure \(\PageIndex{2}\): A new history is empty. Nothing is displayed in the web browser.
    History stream progression 2.
    Figure \(\PageIndex{3}\): The user opens to page 1.
    History stream progression 3.
    Figure \(\PageIndex{4}\): The user clicks on a link to page 2.
    History stream progression 4.
    Figure \(\PageIndex{5}\): The user clicks on a link to page 3.
    History stream progression 5.
    Figure \(\PageIndex{6}\): The user clicks on the Back button. They are now viewing page 2 again.
    History stream progression 6.
    Figure \(\PageIndex{7}\): The user clicks again the back button. Page 1 is now displayed.
    History stream progression 7.
    Figure \(\PageIndex{8}\): From page 1, the user clicks on a link to page 4. The history forgets pages 2 and 3.

    This behaviour can be implemented using a ReadWriteStream.

    Object subclass: #History
        instanceVariableNames: 'stream'
        classVariableNames: ''
        package: 'PBE-Streams'
        
    History >> initialize
        super initialize.
        stream := ReadWriteStream on: Array new.
    

    Nothing really difficult here, we define a new class which contains a stream. The stream is created during the initialize method.

    We need methods to go backward and forward:

    History >> goBackward
        self canGoBackward
            ifFalse: [ self error: 'Already on the first element' ].
        stream skip: -2.
        ^ stream next.
    
    History >> goForward
        self canGoForward
            ifFalse: [ self error: 'Already on the last element' ].
        ^ stream next
    

    Up to this point, the code is pretty straightforward. Next, we have to deal with the goTo: method which should be activated when the user clicks on a link. A possible implementation is:

    History >> goTo: aPage
        stream nextPut: aPage.
    

    This version is incomplete however. This is because when the user clicks on the link, there should be no more future pages to go to, i.e., the forward button must be deactivated. To do this, the simplest solution is to write nil just after, to indicate that history is at the end:

    History >> goTo: anObject
        stream nextPut: anObject.
        stream nextPut: nil.
        stream back.
    

    Now, only methods canGoBackward and canGoForward remain to be implemented.

    A stream is always positioned between two elements. To go backward, there must be two pages before the current position: one page is the current page, and the other one is the page we want to go to.

    History >> canGoBackward
        ^ stream position > 1
    
    History >> canGoForward
        ^ stream atEnd not and: [stream peek notNil ]
    

    Let us add a method to peek at the contents of the stream:

    History >> contents
        ^ stream contents
    

    And the history works as advertised:

    History new
        goTo: #page1;
        goTo: #page2;
        goTo: #page3;
        goBackward;
        goBackward;
        goTo: #page4;
        contents
    >>> #(#page1 #page4 nil nil)
    

    This page titled 12.3: Streaming over Collections is shared under a CC BY-SA 3.0 license and was authored, remixed, and/or curated by via source content that was edited to the style and standards of the LibreTexts platform; a detailed edit history is available upon request.