Concepts

A stream provides sequential access to an aggregate of data, such as a Dylan sequence or a disk file. Streams grant this access according to a metaphor of reading and writing: elements can be read from streams or written to them.

Streams are represented as Dylan objects, and all are general instances of the class <stream>, which the Streams library defines.

We say that a stream is established over the data aggregate. Hence, a stream providing access to the string "hello world" is said to be a stream over the string "hello world".

Streams permitting reading operations are called input streams. Input streams allow elements from the underlying data aggregate to be consumed. Conversely, streams permitting writing operations are called output streams. Output streams allow elements to be written to the underlying data aggregate. Streams permitting both kinds of operations are called input-output streams.

The library provides a set of functions for reading elements from an input stream. These functions hide the details of indexing, buffering, and so on. For instance, the function read-element reads a single data element from an input stream.

The following expression binds stream to an input stream over the string "hello world":

let stream = make(<string-stream>, contents: "hello world");

The first invocation of read-element on stream returns the character 'h', the next invocation 'e', and so on. Once a stream has been used to consume all the elements of the data, the stream is said to be at its end. This condition can be tested with the function stream-at-end?. The following code fragment applies function to all elements of the sequence:

let stream = make(<sequence-stream>, contents: seq);
while (~stream-at-end?(stream)) 
	function(read-element(stream));
end;

When all elements of a stream have been read, further calls to read-element result in the <end-of-stream-error> condition being signalled. An alternative end-of-stream behavior is to have a distinguished end-of-stream value returned. You can supply such an end-of-stream value as a keyword argument to the various read functions; the value can be any object. Supplying an end-of-stream value to a read function is more efficient than asking whether a stream is at its end on every iteration of a loop.

The library also provides a set of functions for writing data elements to an output stream. Like the functions that operate upon input streams, these functions hide the details of indexing, growing an underlying sequence, buffering for a file, and so on. For instance, the function write-element writes a single data element to an output stream.

The following forms bind stream to an output stream over an empty string and create the string "I see!", using the function stream-contents to access all of the stream's elements.

let stream = make(<byte-string-stream>, direction: #"output");
write-element(stream, 'I');
write-element(stream, ' ');
write(stream, "see");
write-element(stream, '!');
stream-contents(stream);

Calling write on a sequence has the same effect as calling write-element on all the elements of the sequence. However, it is not required that write be implemented directly in terms of write-element; it might be implemented more efficiently, especially for buffered streams.

Some streams are positionable; that is, they permit random access to their elements. Postionable streams allow you to set the position at which the stream will be accessed by the next operation. The following example uses positioning to return the character 'w' from a stream over the string "hello world":

let stream = make(<string-stream>, contents: "hello world");
stream-position(stream) := 6;
read-element(stream);

The following example returns a string, but the contents of the first ten characters are undefined:

let stream = make(<string-stream>, direction: #"output");
adjust-stream-position(stream, 10); 
write(stream, "whoa!");
stream-contents(stream);

You can request a sequence containing all of the elements of a positionable stream by calling stream-contents on it. The sequence returned never shares structure with any underlying sequence that might be used in future by the stream. For instance, the string returned by calling stream-contents on an output <string-stream> will not be the same string as that being used to represent the string stream.

When making an input <string-stream>, you can cause the stream to produce elements from any subsequence of the supplied string. For example:

read-to-end(make(<string-stream>, 
                 contents: "hello there, world",
                 start: 6, 
                 end: 11));

This example evaluates to "there". The interval (start, end) includes the index start but excludes the index end. This is consistent with standard Dylan functions over sequences, such as copy-sequence. The read-to-end function is one of a number of convenient utility functions for operating on streams and returns all the elements up to the end of the stream from the stream's current position.