The I/O subsystem provides standard functions for reading and writing files and devices. In particular, the subsystem provides:
The I/O system has three layers of interface. From top to bottom, they are
All conforming implementations must provide two instances of the I/O stacks: one for binary data and one for text. The latter provides a few additional operations to better support text oriented I/O. The library defines optional functors for building new I/O stacks.
Input streams can be viewed as being in one of three states: active, truncated or closed. When initially created, the stream is active. Getting access to the underlying primitive reader (getReader) causes the stream to be truncated. Closing a stream causes the stream to be closed. A closed stream is also truncated.
Each input stream f can be viewed as a sequence of ``available'' elements (the buffer or sequence of buffers) and a mechanism (the reader) for obtaining more. After an operation (v, f') = input(f)
it is guaranteed that v is a prefix of the available elements. In a truncated input stream, there is no mechanism for obtaining more, so the ``available'' elements comprise the entire stream. Reading from a truncated input stream will never block; after all buffered elements are read, input operations always return empty vectors.
Output streams can be viewed as being in one of three states: active, terminated or closed. When initially created, the stream is active. Getting access to the underlying primitive writer causes the stream to be terminated. Closing a stream causes the stream to be closed. A closed stream is also terminated. In a terminated output stream, there is no mechanism for outputting more, so any output operations will raise the IO.Io exception.
It is possible that a stream's underlying reader/writer, or its operating system file descriptor, could be closed while the stream is still active. When this condition is detected, the stream should raise the IO.Io exception with cause
set to IO.ClosedStream.
In Unix, and perhaps in other operating systems, there is no notion of ``end-of-stream.'' Instead, by convention a read
system call that returns zero bytes is interpreted to mean end of stream. However, the next read to that stream could return more bytes. This situation would arise if, for example,
let val z = TextIO.StreamIO.endOfStream f val (a,f') = TextIO.StreamIO.input f val x = TextIO.StreamIO.endOfStream f' in x=z (* not necessarily true! *) endwhereas the following is guaranteed to be true:
let val z = TextIO.StreamIO.endOfStream f val (a,f') = TextIO.StreamIO.input f val x = TextIO.StreamIO.endOfStream f (* note, no prime! *) in x=z (* guaranteed true! *)Thus, the notion of ``end-of-stream'' for an input stream corresponds to a condition on the stream, rather than a place in the stream. For untruncated input streams, when an input operation returns an empty vector (or endOfStream returns
true
), this means that we are currently at the end of the stream. If further data are appended to the underlying file or stream, the next input operation will deliver new elements. Thus, a file may have more than one ``end-of-stream.'' If ``end-of-stream'' condition holds, an input will return the empty vector but the ``end-of-stream'' condition may become false as a result of this input operation.
Note that, after all buffered input is read from a truncated input stream, the input stream remains in a permanent end-of-stream condition.
The semantics of the imperative I/O level can be given by defining imperative streams as references to the underlying stream I/O stream types, and delegating I/O operations to that level. In addition, input at the imperative I/O level rebind the reference to the new ``lazy stream.'' For example, part of a structure matching IMPERATIVE_IO might look like:
structure ImperativeIO : IMPERATIVE_IO = struct structure StreamIO : STREAM_IO = ... datatype instream = INS of StreamIO.instream ref datatype outstream = OUTS of StreamIO.outstream ref fun input (INS(i as ref ins)) = let val (v, ins') = StreamIO.input ins in i := ins'; v end fun output (OUTS(ref outs), v) = StreamIO.output (outs, v) ... end
Text streams (TextIO) contain lines of text and control characters. Text lines are terminated with #"\n"
characters.
In some environments, the external representation of a text file is different from its internal representation: for example, in MS-DOS, text files on disk contain CR-LF ("\r\n"
), and in memory contain only LF ("\n"
) at the end of each line. Thus, on input, the CR-LF
or CR
terminators are translated to a single #"\n"
character. The inverse translation is done on output. More substantial translation will be done on systems that support, for example, escape-coded Unicode text files.
Binary streams (BinIO) match the external files byte for byte.
All streams created by open functions in TextIO and BinIO will be closed (and the outstreams among them flushed) when the SML program exits. The outstreams TextIO.stdOut and TextIO.stdErr will be flushed, but not closed, on program exit.
Last Modified September 1, 1997
Comments to John Reppy.
Copyright © 1997 Bell Labs, Lucent Technologies