On 19/11/2011 4:02 AM, David Rajchenbach-Teller wrote:
Let's start with a little safety vocabulary, not specifically related to
Rust. It is quite common to have two distinct words: "exceptions" for
exceptional but expected behavior – that you typically want to catch –
Heh. I appreciate the attempt to pick such terminology neutrally, but
"exception" and "catch" very plainly points towards the modern
typed-exceptions-with-unwind-semantics implementation.
There are many other systems for error management: error codes, monads,
conditions-and-restarts, signals-and-handlers, etc. etc.
Unwinding-and-catching is a very specific strategy.
I point all this out because, quite a long while ago (before publishing
anything), the Rust-on-paper design I worked on had a non-unwind-based
condition system; errors represented an opportunity for a
dynamically-scoped handler to offer recovery, *or fail*.
and "failures" for issues that are beyond the control of the developer
and can at best be contained by shutting down a system and relaunch it.
To attain a high level of safety, having a manner of dealing with both
it is a Good Thing (tm).
>
Fortunately, these two concepts mostly map to your points 1. or 2.:
Agreed. There are at least expected-and-possibly-handled things and
unexpected-or-definitely-can't-handle things. 1. and 2. :)
For category 2., as you mention, Rust has the mechanism of tasks and
failures. The non-spawnability of closures has me a little worried, but
I am sure that most/all useful cases can be encoded without this feature
and since my current hands-on experience with Rust tasks and failures is
essentially non-existing, I feel incompetent to discuss these in depth.
Closures will be spawnable when we've implemented unique closures.
That's a WIP, not a permanently-missing-feature.
However, this whole thread was about category 1 and, more precisely,
about library-design of category 1, rather than language-design.
Ok.
For reporting exceptional behaviors, the "wonderful tag things" you
mention are necessary, and a sufficient *language* mechanism, but
stopping the *library* design at this point is inviting either the same
mess as Haskell or OCaml or the same mess as mozilla-central. Both
Haskell and OCaml have around 6 distinct – and largely type-incompatible
– manners of reporting exceptions. This does not even take into account
the fact that both OCaml and Haskell sum types are (or can be made) more
flexible/powerful than Rust tags, something we probably do not want in
Rust. On the other side, mozilla-central has only one mechanism, which
has a fixed set of exceptions, and new exceptions can only be added by
rebuilding the whole platform.
In order to avoid both pitfalls, I advocate that we need to decide of a
standard manner of reporting exceptions very early in the development of
Rust – ideally before anybody starts writing or using any library that
makes heavy use of exceptions, such as an IO library.
Yes, and I am proposing one: pass *in* your handlers (or symbolic codes
indicating handler-strategy) and have the callee handle *at the site of
the condition*. Sorry if I wasn't clear enough about the implied use of
tags I meant, up-thread.
I'm serious. I've read and understood what you wrote above, so I'll ask
you do the courtesy of reading and understanding the following
paragraphs fully as well. I'm not writing them not to clobber you with
the Obvious Superiority of my own beliefs -- they may well be wrong --
just to clarify exactly what I'm suggesting, what I'd do as
alternatives, and why.
The old Rust condition system was modeled on the condition system in
Mesa. You named "signals" as global items and gave a syntactic form to
routing a given signal to a locally-defined "handler" in the caller,
much as you would a try/catch block. The difference is that a handler in
this scheme is a typed function-like definition dangling after the
protected block. It reads like so:
try {
os::open(fname);
} handle os::file_not_exist(str filename) -> file {
ret os::create(fname);
}
So the recovery logic remains off the main code path, like a modern
"catch block", but with a fn-like signature: arguments and its own
"recovery value" return type. At signal occurrence, the originating site
would invoke the signal by item name; this would cause the runtime to
find the innermost installed handler via either
"head-of-a-task-local-list" search, or by static code-range search of
the caller stack, similar to C++-unwinding, and call it. The handler
would either return the typed recovery value, or fail. Failure to locate
a handler at all, of course, also generates a fail.
This is a nice pleasant scheme half way between Liskov's CLU exceptions
and lisp's restarts. It has the positive property that it's oriented
towards handling at the signalling site rather than unwinding when you
actually intend to continue; but it introduces fewer moving parts than
the lisp system.
During early review, someone -- I think possibly Brendan? -- pointed out
some retrospective comments -- I think possibly from Lampson? -- on the
system in Mesa. The retrospective was somewhat damning: not of the
system in particular, but of the whole notion of splitting the recovery
path off into a slow-to-invoke secondary handler (as in Mesa but also as
in most modern exception systems).
The retrospective reasoning, IIRC (working from memory here; if Brendan
or whoever pointed it out is reading I'd appreciate a pointer to the
original text) went like this:
- The conditions you expect to generate, the author of the callee
code necessarily can enumerate in their own mind. They invoke the
signals when things go wrong, after all!
- The set of plausible-and-useful recoveries for any given signal is
really quite small and predictable; that same author of the callee
can mentally enumerate all the ways they could expect to be told to
recover anyways.
- If a signal is frequent enough to make it into the API this way,
it's frequent enough that you're going to be invoking the handler
regularly. Having that invocation be hundreds of times slower is
undesirable.
- Having the recovery logic at a distance from the origin and
duplicated for each caller who wants to follow a given pattern
actually leads to buggier, more fragile and less likely recovery.
- The above points combined to -- quite naturally and without
stated intention -- make the programmers using the system gradually
shift any API they designed from using signals to using flags
or variants that described the recovery mode they wanted into
any subsystem with predictable signals.
So they eventually removed the remaining uses of the signal system
(mostly bitrotted) and were happier for it.
I found this argument compelling. So much so I'm probably exaggerating
or mis-stating the arguments a bit. But it lead me to reflect a bit more
on the *realistic* uses of exceptions I've seen in programs, and found
myself unable to debate it: most catch clauses I see do one of a small
number of very predictable things: ignore, retry, hard-fail, log, or try
one of a very small number of alternative strategies to accomplishing
the initial goal (create the file rather than open it, say) that the
author of the callee could very well have predicted and codified in a
small tag-set of extra arguments.
So I removed the condition system from the design docs, and never
implemented it. I propose structuring the libraries along these lines.
That is, to have the callee authors actually think a bit about what an
unusual-return means, which ways there might plausibly be for recovering
from it, and take a tag or vec-of-tags carrying the preferred recovery
strategy.
If this fails to hold together and we really, really have to revive some
kind of structured at-a-distance recovery system, I'm going to suggest
going back to the Mesa-like signal scheme I sketched out earlier (and
above). The main (substantial!) advantage it offers is that recovery
paths cause no actual unwinding-or-destruction -- recovery occurs
effectively "at the signal site" -- so there's no question of
perturbation of the typestate. Unwinding still only happens during
failure. The handler is invoked like any other function and if it
succeeds the unwinder is never even involved. IMO it's much tidier than
try/throw/catch and/or monads-by-macros.
-Graydon
_______________________________________________
Rust-dev mailing list
Rust-dev@mozilla.org
https://mail.mozilla.org/listinfo/rust-dev