LEVIATHAN v962456e · 962456eee1

Library

Streams — the system boundary

Everything that crosses the program boundary (timers, sockets, signals, child processes) is a stream with one consumer; this entry defines the shared model.

since 0.1.0-alpha.1linuxwindows

Description

A Leviathan program does not talk to the outside world through special syntax. Timers, sockets, signals, pipes and files are all library objects that wear the same small stream surface, so one set of ideas covers all of them. This entry describes that model; std.InStream, std.OutStream and std.IOStream describe the three view types in detail.

A stream has three parts.

  • A buffer, StreamBuffer<T>: a single-consumer queue. Producers push values onto it, the consumer takes each value out exactly once, in order, and close() ends it.
  • A write view, OutStream<T>. Its << operator pushes one value and returns the view, so writes chain: out << 1 << 2 << 3 pushes three separate items.
  • A read view, InStream<T>. It reads the buffer with pull(), pullOrNone(), subscribe(callback), or a for loop.

IOStream<T> is both views over one buffer: whatever is written to it can be read back from it. Views do not own different queues; any number of views over the same StreamBuffer share it.

A buffer with a write view and a read view

StreamBuffer<int> buf = StreamBuffer();
OutStream<int> out = OutStream(buf);
InStream<int> inp = InStream(buf);

out << 1 << 2 << 3;
console.writeln("buffered: ${buf.count()}");
console.writeln("pull: ${inp.pull()}");
int? second = inp.pullOrNone();
console.writeln("pullOrNone: ${second ?? -1}");
console.writeln("hasData: ${inp.hasData()}");
inp.pullOrNone();
int? none = inp.pullOrNone();
console.writeln("when empty: ${none ?? -1}");
try {
    inp.pull();
} catch (RuntimeException e) {
    console.writeln("pull on empty: ${e.message}");
}
buffered: 3
pull: 1
pullOrNone: 2
hasData: true
when empty: -1
pull on empty: stream is empty

Pulling from an empty stream with pull() throws; pullOrNone() returns None instead.

Rules

  • One consumer. A stream has a single consumer end. subscribe and iterator() (which for ... in uses) are both standing claims on that end, and whichever runs first wins. A later pull(), subscribe or iterator() throws a RuntimeException that names the claim: consumer end is claimed by a subscriber or consumer end is claimed by an iterator. Delivering one stream to several listeners is something a library builds on top of streams, not a property of the stream itself.
  • Each value is consumed once. Values are removed from the queue as they are read.
  • Closing. close() never throws and may be called any number of times. After a stream is closed, push silently drops the value, so a closed consumer receives no further deliveries. See std.InStream for how closing interacts with values that are still queued.
  • Streams are disposable. InStream<T> implements IDisposable, so a subscription is a resource that using releases on every way out of a scope. Releasing a stream that a producer attached cleanup to (a signal::on stream, for example) runs that cleanup.
  • Waiting is suspension. A for loop over a stream that is open and empty waits for the next value without blocking the rest of the program; see lang.await and lang.tasks. It ends when the stream is closed and drained. A loop over a stream that is never closed waits forever, so bound it with take(n) or close the stream from somewhere.

Examples

A subscription to a signal is a stream, and using releases it. When the last subscriber of a signal is released, the signal returns to its default behavior and the program no longer waits for it:

Releasing a signal subscription with using

void listen() {
    using InStream<int> w = signal::on(signal::USR1);
    console.writeln("subscribed");
}
listen();
console.writeln("released");
subscribed
released

Notes

Timer.ticks() (one tick number per firing) and signal::on(sig) (one value per delivery of the signal) return real InStream<int> values. TcpStream, Process and Pty follow the same shape with callbacks (onData, onClose) and a << or write for the other direction, and a File offers FileInStream/FileOutStream views; those are separate classes, not InStream/OutStream. Console offers the same << operator without being an OutStream.

See also

  • InStream — The reading end of a stream: a queue of values of type T that something else produces.
  • OutStream — The writing end of a stream: you push values of type T in, and a reader takes them out.
  • IOStream — A stream that can be both read and written, with both ends over one queue.
  • Timer — A source of ticks on the event loop.
  • signal — Operating-system signals delivered to the program as streams.
  • IDisposable — The interface of an object that must be cleaned up when its work is finished.