Hi all,
This is going to be long, but I've tried to organise my thoughts clearly
and as succinctly as possible.
On 26/04/13 18:06, Patrick Walton wrote:
So here are my ideas. They are very worse-is-better at this point.
* Failing to perform a basic I/O operation should result in a call to
the `io_error` condition (default behavior: fail task) and should set
an internal flag saying that this stream is dead.
Agreed.
Stream constructors like `open` should return a dead stream.
This would be a relatively ugly approach, to my way of thinking. Why
should a dead stream be returned at all, if the code to create it
failed? Why should I be able to call write() on something that could
not be created?
Operations like read or write on dead streams should be no-ops.
I think this should cause a task failure (maybe even a hard program
assertion failure), rather than doing nothing, BUT, it should be almost
impossible to cause, because nothing should return a bad stream, and
code that "breaks" a stream should be forced to deal with it or be
unwound, rather than simply continuing.
* Stream readers such as `read_uint` should be duplicated into two
methods for convenience: `read_uint` and `try_read_uint`. The former
just delegates to the latter with `.get()`.
This is pretty bad, imho -- enough to spur me into writing long emails
with alternatives ;)
That system would cause a massive development burden for library
writers/users, create much larger APIs, create too many similar
functions, etc. It would mean code duplication, more bugs, more attack
vectors, more confusion, and so on. I've used APIs like this, and
they're not fun. Nor is working on a code base that uses them
inconsistently.
For instance, why should a try_read_uint exist, if you can:
// assign a handler for read failures
{ read(); }
// remove the handler
AND have that handler code implemented just once, in a library?
The only problems I can see are that:
1. Handlers for x() might be slow compared to a if try_x() statement.
2. The "do condition::cond.trap(handlerCode) actualcode" syntax is
pretty unwieldy, and I'm not sure I like it being right at the top
of the do block, potentially a long way from the code it affects.
I think both of those should be addressed at a language/core level,
rather than duplicating lots of functions in every module/lib. I tend
to think, if a few developers can do something once, to save tens or
hundreds or thousands of users doing the same thing, all in their own
slightly different ways, with their own bugs, time loss,
incompatibilities, etc., then it should be done right the first time.
So, FWIW, since aspirational examples were mentioned, I would aim for
something more like this:
try {
handle io::conditions::diskFull with io::handlers::purgeTempFiles;
let fp = io::open(somePath, io::mode::write);
fp.write(someBytes);
} catch (cond : &io::Condition) {
io::println(fmt!(
"Some non-disk-full error (%?) occured, OR we couldn't
purge disk space."
"I don't know how to handle this. Rethrowing.",
cond
));
raise cond;
} finally {
// Restores the default handler state before we changed it above.
// it might restore core::throwHandler as the handler, for example,
// which just throws up the stack and unwinds, like other
language's
// exceptions would.
//
// NOTE: To reduce boilerplate, this should probably be
implicit in
// exiting the try block, rather than requiring a
manual finally
// block entry.
handle io::cond::diskFull with super;
}
In other words, I'm imagining a language where:
* You have Conditions, built by ConditionFactories. The casual user
doesn't need to care, he just uses the API and handles its Conditions.
* ConditionFactories interface with the call stack to allow building a
stack of Handlers for the Condition.
* Conditions are ALSO usable just like exceptions: they're /throwable/
& /catchable/, by naming the specific class you want to catch, or by
naming a base class/trait.
* The only real difference between catching a thrown Condition and
handling one would be that:
o In-place Condition handling is done with a closure or a function
call
o Thrown Conditions first use a special in-place Handler that
unwinds the stack, passing the condition up the stack to a catch
block, or, if no more catch blocks exist, to terminate() (or
Rust's equivalent).
* If you don't set a Handler, you get the default behaviour defined in
that Condition class, or, if that is not set, the language/library
default for /any/ Condition class, which is probably throwing, with
the *option* of catching.
* While a Condition can be caught by catch() {}, handled by a
function, or handled by a closure, all three methods take a
compatible argument: a Condition trait, or a variation. This allows
code sharing, aids familiarity, etc.
* Condition handling code ends by calling one of:
cond.retry()
To attempt to retry the code that raised the condition. I
confess, I've little knowledge of how this works in Rust,
but I imagine it's something like a guarded try block, with
potential to become like STM.
raise cond
To throw the condition to a catch () block further up the stack
* The Condition trait would support retrieving the stack frame, the
file/line which the condition occurred at, etc., through methods.
* In the case of catch blocks, some of that information would have
been built up during the stack unwind, before the information was lost.
* Conditions passed to in-place code where no stack unwinding is
needed, would LOOK the same to the user, but they'd be FASTER, because:
o The handling code would (interchangeably) be closures or functions.
o That code would ideally be inlined whenever advantageous.
o The Condition object would not be built up ahead of time, since
it might not be needed, and all the information would be there
in the current and previous stack frames anyway. There's no need
to package all that information unless the Condition is thrown.
*_Variations_*_
_
* Some kind of tracking of previously set handlers is needed. This
could be as simple as setting tempory variables. However, it could
be useful to allow a full stack of handlers, on a per-call-stack
basis, so that raise cond in inline handlers calls the previous
handler, until it reaches the throw handler, and then catch blocks
take over. Using raise cond in those rethrows cond up the stack.
* As a less invasive proposal, to avoid adding the handle keyword,
this sort of syntax could be used:
try {
io::cond::diskFull.push_handler(io::handlers::purgeTempFiles)
// some code
} finally {
io::cond::diskFull.pop_handler();// again, this could be
implicit if pushes happen in the try block
}
This is closer to Rust's existing condition handling (if I've been
looking at the latest version). However, I think it's less elegant,
even if it does save keywords. IMHO, error handling is SUCH a big,
frequent thing in languages, that it deserves a few keywords of its own.
The full concept above would require only two new keywords: "handle",
and "catch", along with some simple new statements. I'm not qualified
to implement it in a compiler, but I believe it would be minimal effort,
over the basic throwable exception model. With the alternative syntax,
only "catch" would be new, and optional.
To me, the extra keywords/work seem like a small price, for what we'd
gain in clarity, elegance, and flexibility. We'd have the best of two
popular error handling models, to choose from at will, with a nice
syntax, too.
--
Lee
_______________________________________________
Rust-dev mailing list
Rust-dev@mozilla.org
https://mail.mozilla.org/listinfo/rust-dev