Streams Library

Previous Page TOC Index Next Page See Page

Mindy Compiler Mindy Debugger Mindy Object Extensions Streams Library Standard IO Print Library Format Library Melange Interface TK Library Collection extensions Table Extensions String extensions Regular Expressions Transcendental Library Time Library Random Library Matrix Library


The Streams Library

Authors: Scott McKay, Bill Chiles, Marc Ferguson, and Eliot Miranda.

Note: This document was written by Harlequin, Inc., and not by the Gwydion Project.

Editor: Andrew Shires.

Date: 06 Feb 1997.

0.1 About this document

0.1.1 Acknowledgments

We’d like to thank the other people who have been instrumental in the production of this proposal: Jonathan Bachrach, Dave Berry, John Dunning, Chris Fry, Paul Haahr, William Lott, Rob Maclachlan, Tim McNerney, Tony Mann, Keith Playford, Robert Stockton, and Tucker Withington.

0.1.2 Discussing error conditions

This document uses two special terms in discussions of error conditions.

When we say that something is an error, we mean that the result is undefined. In particular, we do not mean that a Streams implementation must signal an error condition; that is the implementor’s choice. So, for instance, the following text, from page 79, means only that the result of using unread-element in the case described is undefined:

Only when we specifically mention signaling do we mean that a Streams implementation must signal an error condition. Note that we may not, in such a case, say exactly which error condition must be signaled; if we do not say so, the choice is again up to the implementor. In this text from the description of stream-position-setter on page 77, for instance, we state that an implementation must signal an error, but we do not say what error must be signaled:

By contrast, the following text from the description of read-element on page 67 says exactly which error must be signaled:


0.2 Goals of the library

The Dylan Streams library aims to provide:

The proposal presents the design of a Streams library that meets these goals using Dylan’s built-in sequences and a buffered disk file interface.

The proposal does not address a number of related issues, including:


0.3 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.

0.3.1 Streams, growing sequences, and object identity

When writing to output streams over sequences, Dylan may from time to time need to grow the underlying sequence that it is using to represent the stream data.

Consider the example of an output stream instantiated over an empty string. As soon as a write operation is performed on the stream, it is necessary to replace the string object used in the representation of the string stream. As well as incurring the cost of creating a new string, the replacement operation can affect the integrity of other references to the string within the program.

To guarantee that alias references to a sequence used in an output <sequence-stream> will have access to any elements written to the sequence via the stream, supply a <stretchy-vector> to make. A stream over a stretchy vector will use the same stretchy vector throughout the stream’s existence.

For example:

let sv = make(<stretchy-vector>);
let stream = make(<sequence-stream>, 
                           contents: sv, 
                           direction: #"output");
write(stream,#(1, 2, 3, 4, 5, 6, 7, 8, 9));
write(stream,"ABCDEF");
values(sv, stream-contents(stream));

The example returns two values. Each value is the same (\==) stretchy vector:

(1, 2, 3, 4, 5, 6, 7, 8, 9, ’A’, ’B’, ’C’, ’D’, ’E’, ’F’)

If a stretchy vector is not supplied, the result is different:

let v = make(<vector>, size: 5);
let stream = make(<sequence-stream>,
                           contents: v, 
                           direction: #"output");
write(stream,#(1, 2, 3, 4, 5, 6, 7, 8, 9));
write(stream,"ABCDEF");
values(v, stream-contents(stream));

This example returns as its first value the original vector, whose contents are undefined, but the second value is a new vector:

(1, 2, 3, 4, 5, 6, 7, 8, 9, ’A’, ’B’, ’C’, ’D’, ’E’, ’F’)

This difference arises because the output stream in the second example does not use a stretchy vector to hold the stream data. A vector of at least 15 elements is necessary to accommodate the elements written to the stream, but the vector supplied, v, can hold only 5. Since the stream cannot change v’s size, it must allocate a new vector each time it grows.

0.4 Stream classes

The exported streams class heterarchy is as follows:

Open abstract class

<file-stream>

Open abstract class

<sequence-stream>

Open instantiable class

<string-stream>

Open instantiable class

<byte-string-stream>

Open instantiable class

<unicode-string-stream>

Open instantiable class

0.4.1 Creating streams

The following functions are used to create streams.

0.4.1.1 File streams

File streams are intended only for accessing the contents of files. They are not intended to provide a general file handling facility of renaming, deleting, moving, parsing directory names and so on.

make file-stream-class

G.f method

make file-stream-class #key                                    locator direction if-exists if-does-not-exist 
                                    buffer-size element-type encoding 
=> file-stream-instance 

type-for-file-stream

Open generic function

type-for-file-stream locator element-type #rest all-keys => file-stream-type 

0.4.1.2 Options when creating file streams

When creating file streams, you can can supply the following init-keywords to make in addition to the usual direction:

The if-exists init-keyword allows you to specify an action to take if the file named by locator already exists. The options are:

#"new-version"

If the underlying file system supports file versioning, a new version of the file is created. This is the default when the stream’s direction is #"output".

If the file system does not support file versioning, the implementation should substitute one of the other if-exists behaviors; the #"replace" behavior is a good choice.

The if-does-not-exist init-keyword allows you to specify an action to take if the file named by locator does not exist. The options are:

Because creating a file stream always involves an attempt to open the underlying file, the aforementioned error conditions will occur during file stream instance initialization.

If an implementation checks file permissions when creating and opening file streams, and it detects an attempt to read a file for which the user has no read permission, or to write a file for which the user has no write permission, then an <invalid-file-permissions-error> condition is signalled at the time the file stream is created.

The element-type init-keyword controls how the elements of the underlying file are accessed.

Implementation Note: Ideally, element-type could be any valid Dylan type such as limited(<integer>, min: 0, max: 255) or <unicode-character>. This approach may make it possible to implement a potentially inefficient but general set of file streams. Unfortunately the current language definition does not include adequate support for this approach, so we specify instead an interim minimum set of three element types. The element types are for the time being exported from the streams module of the Streams library.

The three possible element types are:

<byte-character>

The file is accessed as a sequence of 8-bit characters.

<unicode-character>

The file is accessed as a sequence of 16-bit Unicode characters.

0.4.1.3 Sequence streams

There are make methods on <sequence-stream>, <string-stream>, <byte-string-stream> and <unicode-string-stream>. The make methods on <sequence-stream> and <string-stream> might not create direct instances of those classes, but instead an instance of a subclass determined by type-for-sequence-stream.

make sequence-stream-class

G.f. method

make sequence-stream-class #key contents direction start end 
=> sequence-stream-instance 

type-for-sequence-stream

Open generic function

type-for-sequence-stream sequence => sequence-stream-type 

make string-stream-class

G.f. method

make string-stream-class #key contents direction start end 
=> string-stream-instance 

make byte-string-stream-class

G.f. method

make byte-string-stream-class #key contents direction start end 
=> byte-string-stream-instance 

make unicode-string-stream-class

G.f. method

make unicode-string-stream-class #key contents direction start end 
=> unicode-string-stream-instance 

0.4.2 Closing streams

When creating new stream classes it may be necessary to add a method to the close function, even though it is not part of the Stream Extension Protocol.

close

Open generic function

close stream #key #all-keys => () 

0.5 Reading and writing from streams

It is an error to call any of these functions on a buffered stream while its buffer is held.

0.5.1 Reading from streams

The following are the basic functions for reading from streams. To implement a new input stream that is not a <buffered-stream>, you must provide methods for read-element, stream-input-available?, peek, read, read-into!, and discard-input. If you implement a new stream that is a <positionable-stream>, you might need to supply a new method for unread-element.

read-element

Open generic function

read-element input-stream #key on-end-of-stream => element-or-eof 

peek

Open generic function

peek input-stream #key on-end-of-stream => element-or-eof 

read

Open generic function

read input-stream n #key on-end-of-stream => sequence-or-eof 

read-into!

Open generic function

read-into! input-stream n sequence #key start on-end-of-stream => count-or-eof 

discard-input

Open generic function

discard-input input-stream => () 

stream-input-available?

Open generic function

stream-input-available? input-stream => available? 

0.5.2 Convenience functions for reading from streams

The following is a small set of convenient reading functions that search for particular elements in a stream. These functions behave as though they were implemented in terms of the more primitive functions described in Section 0.5.1.

read-to

Function

read-to input-stream element #key on-end-of-stream test => sequence-or-eof found? 

read-through

Function

read-through input-stream element #key on-end-of-stream test 
=> sequence-or-eof found? 

read-to-end

Function

read-to-end input-stream => sequence 

skip-through

Function

skip-through input-stream element #key test => found? 

0.5.3 Writing to streams

The following are the basic functions for writing to streams.

To implement a new output stream that is not a <buffered-stream>, you must provide methods for write-element, write, force-output, and discard-output.

write-element

Open generic function

write-element output-stream element => () 

write

Open generic function

write output-stream sequence #key start end => () 
   do (method (elt) write-element(stream, elt) end, sequence); 
   sequence;

force-output

Open generic function

force-output output-stream => ()

synchronize-output

Open generic function

synchronize-output output-stream => () 

discard-output

Open generic function

discard-output output-stream => ()

0.5.4 Reading and writing by lines

The following functions facilitate line-based input and output operations.

The newline sequence for string streams is a sequence comprising the single newline character \n. For character file streams, the newline sequence is whatever sequence of characters the underlying platform uses to represent a newline. For example, on MSDOS platforms, the sequence might comprise two characters: a carriage return followed by a linefeed.

Implementation Note: The functions described in this section are potentially an interim solution to one aspect of the more general problem of encoding and data translation. At some point, these functions may be moved into, or subsumed by, another higher level library that deals with the encoding problems in a better way. Note that no other functions in the Streams library do anything to manage the encoding of newlines; calling write-element on the character \n does not cause the \n character to be written as the native newline sequence, except by coincidence.

read-line

Open generic function

read-line input-stream #key on-end-of-stream => string-or-eof newline? 

read-line-into!

Open generic function

read-line-into! input-stream string #key start on-end-of-stream grow? => string-or-eof newline? 

write-line

Open generic function

write-line output-stream string #key start end => () 

new-line

Open generic function

new-line output-stream => ()

0.5.5 Querying streams

The following functions can be used to determine various properties of a stream.

To implement a new stream you must provide methods for stream-open?, stream-at-end?, and stream-element-type.

stream-open?

Open generic function

stream-open? stream => open? 

stream-element-type

Open generic function

stream-element-type stream => element-type 

stream-at-end?

Open generic function

stream-at-end? stream => boolean 

0.5.6 Positionable stream protocol

The following comprises the protocol for positionable streams.

To implement a new positionable stream you must provide methods for stream-position, stream-position-setter, adjust-stream-position, stream-size, stream-contents, and unread-element.

A stream position can be thought of as a natural number that indicates how many elements into the stream the stream’s current location is. However, it is not always the case that a single integer contains enough information to reposition a stream. Consider the case of an "uncompressing" file stream that requires additional state beyond simply the file position to be able to get the next input character from the compressed file.

The Streams library addresses this problem by introducing the class <stream-position>, which is subclassed by various kinds of stream implementations that need to maintain additional state. A stream can be repositioned as efficiently as possible when stream-position-setter is given a value previously returned by stream-position on that stream.

It is also legal to set the position of a stream to an integer position. However, for some types of streams, to do so might be slow, perhaps requiring the entire contents of the stream up to that point to be read.

<stream-position>

Abstract class

stream-position

Open generic function

stream-position positionable-stream => position 

stream-position-setter

Open generic function

stream-position-setter position positionable-stream => new-position 

adjust-stream-position

Open generic function

adjust-stream-position positionable-stream delta #key from => new-position 

as

G.f. method

as integer-class stream-position => integer 

stream-size

Open generic function

stream-size positionable-stream => size 

stream-contents

Open generic function

stream-contents positionable-stream #key clear-contents? => sequence 

unread-element

Open generic function

unread-element positionable-stream element => element 

0.5.7 Using file streams

The following are operations that pertain to file streams.

close

G.f. method

close file-stream #key abort wait? 

with-open-file

Macro

with-open-file (stream-var = locator, #rest keys) body end 
as(<locator>, locator)
with-open-file (fs = ("foo.text", element-type: <byte>))
   read-to-end(fs)
end;
begin
   let hidden-fs = #f;     // In case the user bashes fs variable
   block ()
      hidden-fs := make(<file-stream>, 
                              locator: "foo.text", element-type: <byte>);
      let fs = hidden-fs;
      read-to-end(fs);
   cleanup
      if (hidden-fs) close(hidden-fs) end;
   end block;
end;

0.6 Locking streams

Stream locks have multilocking semantics. That is, a single thread can lock a stream more than once, but the thread must unlock the stream for each time it locked the stream. Furthermore, threads waiting for a stream lock are expected to do so by blocking, not by "spinning".

This allows a high-level printing routine to lock a stream across several calls to output functions, ensuring all the output is contiguous at the stream’s destination. For example, the write-line function locks its stream argument and then calls the write and new-line functions. The write function locks its stream argument by calling get-output-buffer, but because of the multilocking semantics, the call to write within write-line does not block waiting for a lock. The same thing happens with new-line. Before returning, write-line unlocks the stream so that other routines may call output functions on the stream or get the stream’s buffer for direct manipulation.

The Locking Protocol isolates access to a stream so that only one thread may use the stream at any time. In a single-threaded Dylan implementation, these functions do nothing.

The Buffer Access Protocol functions that get a buffer first lock the stream, and those functions that release the buffer unlock the stream. Thus, getting a buffer both isolates access to the stream for a single thread and ensures that the single thread does not try to get the stream’s buffer multiple times while already holding the buffer.

The Buffer Access Protocol isolates access to a buffer within a single thread to prevent reentrancy problems and programming mistakes. Essentially, the lightweight buffer locking ensures that applications do not call output functions that directly manipulate a stream’s buffer from within routines that are already directly manipulating the stream’s buffer. This situation must be forbidden because the inner call to get the buffer cannot reliably return the state of the stream’s buffer while the application already holds the buffer.

stream-locked?

Function

stream-locked? stream => boolean 

lock-stream

Function

lock-stream stream => () 

unlock-stream

Function

unlock-stream stream => ()

with-stream-locked

Macro

with-stream-locked (stream) body end

0.7 Using buffered streams

A goal of the streams library is to provide efficient support for general use of buffered I/O. At the same time, programmers using the library should not need to be concerned with buffering in most cases. For most uses of buffered streams, the buffering is transparent, but programs requiring more control can access buffering functionality when appropriate. This section describes the available buffering functionality.

0.7.1 Overview

A buffered stream maintains some sort of buffer. All buffered streams use the sealed class <buffer> for their buffers. You can suggest a buffer size when creating buffered streams, but normally you do not need to do so. Streams implementations should choose buffer sizes that are appropriate for the stream’s source or destination.

Instances of the class <buffer> also contain some state information. This state information includes an index where reading or writing should begin, and an index that is the end of input to be read, or the end of space available for writing.

Buffered streams also maintain a held state, indicating whether the application has taken the buffer for a stream and has not released it yet. When a thread already holds the buffer for a stream, it is an error to get the buffer again (or any other buffer for the same stream); this property holds for the one thread of a single-threaded Dylan implementation as well as for multi-threaded implementations.

0.7.2 Buffer access protocol

This section describes the functions that users of buffered streams invoke to manipulate stream buffers directly. These functions use corresponding functions from the Stream Extension Protocol to do their work. For example, get-input-buffer calls do-get-input-buffer. All Stream Extension Protocol functions are named with a do- prefix on the corresponding Buffer Access Protocol function. Users should never call the Stream Extension Protocol directly.

Threaded Dylan implementations should place system-dependent mutual exclusion calls in the Buffer Access Protocol functions. All streams implementations are encouraged to check the buffer-held state in the Buffer Access Protocol functions, and these functions should signal an error if the buffer is already held. The separation of the Buffer Access Protocol and the Stream Extension Protocol allows users to more portably extend the buffered stream protocol to new stream types. Programmers avoid the following design and maintenance hassles:

Programs that manipulate buffers are entirely responsible for tracking the buffer’s state and informing the stream of any changes to the buffer’s state. The technique for using a buffer is to get the input or output buffer and its bounds, consume or produce data, update the buffer’s state, and then release the buffer. The new bounds are indicated by calling buffer-next-setter and buffer-end-setter. Using buffers directly interoperates with using higher-level functions such as read-element, read, write-element, write, and so on. This is because higher-level stream operations are implemented in terms of the Buffer Access Protocol for instances of <buffered-stream>.

0.7.2.1 Useful types when using buffers

<byte>

Type

<byte-character>

Type

<unicode-character>

Type

<byte-vector>

Type

<buffer>

Sealed instantiable class

<buffer-index>

Type

0.7.2.2 Using buffers for input

get-input-buffer

Function

get-input-buffer buffered-stream, #key wait?, bytes => buffer-or-false 

release-input-buffer

Function

release-input-buffer buffered-stream => ()

with-input-buffer

Macro

with-input-buffer (buffer-var = exp, #key wait?, bytes) body end;

next-input-buffer

Function

next-input-buffer buffered-stream #key wait?, bytes => buffer-or-false 

input-available-at-source?

Function

input-available-at-source? buffered-stream => available? 

0.7.2.3 Using buffers for output

get-output-buffer

Function

get-output-buffer buffered-stream, #key bytes => buffer 

release-output-buffer

Function

release-output-buffer buffered-stream => () 

with-output-buffer

Macro

with-output-buffer (buffer-var = exp, #key bytes) body end;

next-output-buffer

Macro

next-output-buffer buffered-stream #key bytes => buffer 

0.7.3 Copying to and from buffers

All generic sequence operations work on buffers. The Streams library provides some additional functions to compensate for lacking functionality in Dylan’s sequence operations. There are also a few functions for updating a buffer’s state.

buffer-next

Function

buffer-next buffer => buffer-index 

buffer-next-setter

Function

buffer-next-setter new-value buffer => new-value 

buffer-end

Function

buffer-end buffer => buffer-index 

buffer-end-setter

Function

buffer-end-setter new-value buffer => new-value 

buffer-subsequence

Open generic function

buffer-subsequence buffer result-class start end => sequence 

copy-into-buffer!

Open generic function

copy-into-buffer! buffer buffer-start sequence #key start end => () 

copy-from-buffer!

Open generic function

copy-from-buffer! buffer buffer-start sequence #key start end => ()

0.7.4 Stream extension protocol

These are the generic functions to which implementors of streams add methods when extending the stream protocol to new subclasses of <buffered-stream>. Although the close function is not described here, you might need to add a method to it when implementing a new stream. See close, page 67.

0.7.4.1 Creating new input or input-output streams

do-get-input-buffer

Open generic function

do-get-input-buffer buffered-stream #key wait? bytes => buffer-or-f 

do-release-input-buffer

Open generic function

do-release-input-buffer buffered-stream => () 

do-next-input-buffer

Open generic function

do-next-input-buffer buffered-stream #key wait?, bytes => buffer-or-false 

do-input-available-at-source?

Open generic function

do-input-available-at-source? buffered-stream => available? 

0.7.4.2 Creating new output or input-output streams

do-get-output-buffer

Open generic function

do-get-output-buffer buffered-stream #key bytes => buffer 

do-release-output-buffer

Open generic function

do-release-output-buffer buffered-stream => ()

do-next-output-buffer

Open generic function

do-next-output-buffer buffered-stream #key bytes => buffer 

0.8 Conditions

The class definitions for the error conditions are as follows. There is no recovery protocol defined for any of these errors. Every condition described in this section that takes an init-keyword has a slot accessor for the value supplied, and the name of the accessor function is the name of the condition class (without the angle brackets) to which a hyphen and the name of the init-keyword is appended.

<end-of-stream-error>

Error

<incomplete-read-error>

Error

<file-error>

Error

<file-exists-error>

Error

<file-does-not-exist-error>

Error

<invalid-file-permissions-error>

Error


0.9 Wrapper streams

Sometimes stream data requires conversion before an application can use it: you might have a stream over a file of EBCDIC characters which you would prefer to handle as their ASCII equivalents, or you might need to encrypt or decrypt file data.

Wrapper streams provide a mechanism for working with streams which require such conversion. Wrapper streams hold on to an underlying stream, delegating to it most of the operations that implement streaming. The wrapper stream carries out appropriate processing in its own implementations of the streaming protocol.

The Dylan Streams Library includes a base class called <wrapper-stream> upon which other wrapping streams can be implemented.

define class <wrapper-stream> (<stream>)
   slot inner-stream :: <stream>, 
            required-init-keyword: inner-stream:;
end class;

A subclass of <wrapper-stream> can "pass on" functions such as read-element and write-element by simply delegating these operations to the inner stream:

define method read-element (ws :: <io-wrapper-stream>);
   read-element(ws.inner-stream)
end method;
define method write-element (ws :: <io-wrapper-stream>, element);
   write-element(ws.inner-stream,element)
end method;

Assuming that <io-wrapper-stream> delegates all other operations to its inner stream, the following would suffice to implement a 16-bit Unicode character stream wrapping an 8-bit character stream.

define class <unicode-stream> (<io-wrapper-stream>) end class;
define method read-element (s :: <unicode-stream>) 
   => ch :: <unicode-character>;
   with-stream-locked (s)
      let first-char = read-element(s.inner-stream);
      let second-char = read-element(s.inner-stream)
   end;
   convert-byte-pair-to-unicode(first-char, second-char)
end method;
define method write-element   (s :: <unicode-stream>, 
                                          c :: <character>);
   let (first-char, second-char) = convert-unicode-to-byte-pair(c);
   with-stream-locked (s)
      write-element(s.inner-stream, first-char);
      write-element(s.inner-stream, second-char)
   end;
   c
end method;
define method stream-position (s :: <unicode-stream>) 
   =>  p :: <integer>;
   truncate/(stream-position(s.inner-stream), 2)
end method;
define method stream-position-setter   (p :: <integer>, 
                                                      s :: <unicode-stream>);
   stream-position(s.inner-stream) := p * 2
end method;

0.9.1 Wrapper streams and delegation

One problem with wrapper streams is the need for a wrapper stream to intercept methods invoked by its inner stream. For example, consider two hypothetical streams, <interactive-stream> and <dialog-stream>, the latter a subclass of <wrapper-stream>. Both of these classes have a method called prompt. The <interactive-stream> class specializes read thus:

define method read (   s :: <interactive-stream>, 
                              n :: <integer>,
                              #key on-end-of-stream);
   prompt(s);
   next-method()
end method; 

If a <dialog-stream> is used to wrap an <interactive-stream> then an invocation of read on the <dialog-stream> will call prompt on the inner
<interactive-stream>, not on the <dialog-stream>, as desired. The problem is that the <dialog-stream> wants to delegate some tasks to its inner stream, but handle some other tasks itself.

Some languages, notably Self, support such delegation in the language. Dylan, in keeping with other generic-function-based languages, does not, but experience with streams packages in a number of other generic-function-based languages has shown that it is very useful to provide delegation for wrapper streams. So we provide an explicit implementation of delegation in the streams system.

Delegation is implemented by the use of the outer-stream slot, which is defined in the base class <stream>:

define abstract class <stream> (<object>)
   slot outer-stream :: <stream>, init-keyword: outer-stream:;
end class;

outer-stream is used instead of the stream itself whenever a stream invokes one of its other protocol methods.

Note: A stream must not use inner-stream if performing recursion, since this would cause an infinite regress.

A correct implementation of the read method in the example above would be as follows:

define method read (   stream :: <interactive-stream>, 
                              n :: <integer>
                              #key on-end-of-stream)
   prompt(s.outer-stream);
   next-method()
end method;

The initialize method on <stream> is defined to set the outer-stream slot to be the stream itself. The initialize method on <wrapper-stream> is specialized to set the outer-stream slot to be the "parent" stream:

define method initialize (stream :: <wrapper-stream>,
                         #key on, #rest all-keys);
   an-inner-stream.outer-stream := stream;
   next-method()
end method;

Implementation Note: One disadvantage of this scheme is that it may prevent useful optimizations, such as inlining. If this is an important performance issue in circumstances where wrapper streams are not necessary, then it is relatively simple to provide an implementation of the streams module that omits the delegation mechanism.

0.10 Wrapper stream protocol

<wrapper-stream>

Open instantiable class

inner-stream

Open generic function

inner-stream wrapper-stream => wrapped-stream 

inner-stream-setter

Open generic function

inner-stream-setter stream wrapper-stream => stream 

outer-stream

Open generic function

outer-stream stream => wrapping-stream 

outer-stream-setter

Open generic function

outer-stream-setter wrapper-stream stream => wrapper-stream 

Mindy Compiler Mindy Debugger Mindy Object Extensions Streams Library Standard IO Print Library Format Library Melange Interface TK Library Collection extensions Table Extensions String extensions Regular Expressions Transcendental Library Time Library Random Library Matrix Library

Previous Page TOC Index Next Page See Page

Copyright 1994, 1995, 1996, 1997 Carnegie Mellon University. All rights reserved.

Send comments and bug reports to gwydion-bugs@cs.cmu.edu