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

Reply via email to