The first failure taught the failure channel at a
call site. Generators taught a body that suspends
between values. This page is where the two meet, and the meeting has a
puzzle in it: when a suspended generator runs fail, there is no call
statement to deliver to — the body is frozen mid-conversation, and its real
call site is the consumer’s loop. Mica’s answer: the failure rides the
channel the body already answers through, and the loop carries the
consumption clause.
The example is examples/FallibleStreams
in the tutorial repository.
Getting the file
make -C examples/FallibleStreams runFallibleStreams
pull chain, handled at the loop
reading 10
reading 20
reading 30
producer: cleanup ran
consumer: the sensor saturated
propagated by the summing function
producer: cleanup ran
consumer: saturated after summing what arrived
sum answered -1
ring, handled at the same kind of loop
producer: cleanup ran
reading 10
reading 20
consumer: the sensor saturated, across the ringThe example, walked
The signature, on a generator
type
SensorError = error (Saturated);
generator Readings(limit : int64) : stream of int64 fails SensorError;
begin
defer WriteLn(" producer: cleanup ran");
...
fail Saturated;fails means on a generator exactly what it meant on a function: this body
can end on the failure channel, and the type says so. The defer is the
proof line for the order this page cares about.
The loop carries the clause
for v in Readings(3) do
begin
WriteLn(" reading %lld", v);
end
on fail e do
if e = Saturated then
WriteLn(" consumer: the sensor saturated");The consumption clause sits on the loop, after its end, and offers exactly
the choices a call statement offers: a handler binding the code, on fail leave forwarding it on the consumer’s own channel — the summing function in
the example is that road — and on fail continue discarding it visibly.
Read the first section’s output order: the three readings, then the
producer’s cleanup, then the consumer’s handler. The failure unwound the
producer first — its deferred statements ran on the way out — and only then
surfaced at the pull site. Cleanup and error handling are one mechanism, in
the streams world too.
The ring carries the same word
s : stream of int64 fails SensorError;
...
s := schedule Readings(2) buffer 4;Schedule the same fallible body behind a bounded ring and the story holds: the failure crosses the ring as the stream’s ending and surfaces at the same kind of loop with the same clause. One detail is worth reading off the output: in the ring section the cleanup line appears before the drained readings. The buffered producer ran ahead, failed, and unwound while values still sat in the ring — the failure waits in line behind them, and the consumer hears it exactly once, after draining what was honestly produced.
And note the variable’s type: stream of int64 fails SensorError. A plain
stream of int64 cannot bind a fallible generator — the compiler refuses it
and spells the fix, because a failure that a type quietly dropped would be a
failure nobody consumes.
Try it
1. Drop the domain from the variable. Change the declaration to
s : stream of int64; and rebuild:
analyzer error 5388: the fallible generator 'Readings' cannot bind the stream variable 's', whose type carries no fails domain: declare the variable over the generator's own domain — 'stream of T fails SensorError' — and the for-in over it consumes the failure exactly as the direct call road does2. Leave early. In the first loop, add leave; after the first
reading prints. One reading, then the producer’s cleanup — and no handler
line, because a consumer that abandons a fallible stream consumes no
failure: cancellation routes the body onto a plain leave, and only an
executed fail ever records one. Early exit is not an error, and the
arrangement knows the difference. (Bare leave ends the whole main block,
as init-and-leave teaches — the later sections go
with it, which is why this is an experiment and not the example.)
What the compiler proved
A fallible stream is fallible in its type, at the generator, at the variable, and at the loop — none of the three can quietly disagree. The producer’s cleanup runs before any consumer hears the failure, on the pull chain and across a ring alike. And the loop’s clause is the call statement’s clause: one failure grammar, everywhere a failure can arrive.
Next
Channel streams for the rest of the push-side story, and Error domains for failure types richer than one code. Every example lives in the tutorial repository.