The Right Approach to Exceptions
Jacob Carlborg
doob at me.com
Mon Feb 20 01:32:57 PST 2012
On 2012-02-20 02:03, H. S. Teoh wrote:
> On Sat, Feb 18, 2012 at 11:09:23PM -0500, bearophile wrote:
>> Sean Cavanaug:
>>
>>> In the Von Neumann model this has been made difficult by the stack
>>> itself. Thinking of exceptions as they are currently implemented in
>>> Java, C++, D, etc is automatically artificially constraining how
>>> they need to work.
>>
>> It's interesting to take a look at how "exceptions" are designed in
>> Lisp:
>> http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html
> [...]
>
> I'm surprised nobody responded to this. I read through the article a
> bit, and it does present some interesting concepts that we may be able
> to make use of in D. Here's a brief (possibly incomplete) summary:
>
> One problem with the try-throw-catch paradigm is that whenever an
> exception is raised, the stack unwinds up some number of levels in the
> call stack. By the time it gets to the catch{} block, the context in
> which the problem happened is already long-gone, and there is no other
> recourse but to abort the operation, or try it again from scratch. There
> is no way to recover from the problem by, say, trying to fix it *in the
> context in which it happened* and then continuing with the operation.
>
> Say P calls Q, Q calls R, and R calls S. S finds a problem that prevents
> it from doing what R expects it to do, so it throws an exception. R
> doesn't know what to do, so it propagates the exception to Q. Q doesn't
> know what to do either, so it propagates the exception to P. By the time
> P gets to know about the problem, the execution context of S is long
> gone; the operation that Q was trying to perform has already been
> aborted. There's no way to recover except to repeat a potentially very
> expensive operation.
>
> The way Lisp handles this is by something called "conditions". I won't
> get into the definitions and stuff (just read the article), but the idea
> is this:
>
> - When D encounters a problem, it signals a "condition".
>
> - Along with the condition, it may register 0 or more "restarts",
> basically predefined methods of recovering from the condition.
>
> - The runtime then tries to recover from the condition by:
>
> - Checking to see if there's a handler registered for this condition.
> If there is, invoke the most recently registered one *in the
> context of the function that triggered the condition*.
>
> - If there's no handler, unwind the stack and propagate the condition
> to the caller.
>
> - There are two kinds of handlers:
>
> - The equivalent of a "catch": matches some subset of conditions that
> propagated to that point in the code. Some stack unwinding may
> already have taken place, so these are equivalent to catch block in
> D.
>
> - Pre-bound handlers: these are registered with the runtime condition
> handler before the condition is triggered (possibly very high up
> the call stack). They are invoked *in the context of the code that
> triggered the condition*. Their primary use is to decide which of
> the restarts associated with the condition should be used to
> recover from it.
>
> The pre-bound handlers are very interesting. They allow in-place
> recovery by having high-level callers to decide what to do, *without
> unwinding the stack*. Here's an example:
>
> LoadConfig() is a function that loads an application's configuration
> files, parses them, and sets up some runtime objects based on
> configuration file settings. LoadConfig calls a bunch of functions to
> accomplish what it does, among which is ParseConfig(). ParseConfig() in
> turn calls ParseConfigItem() for each configuration item in the config
> file, to set up the runtime objects associated with that item.
> ParseConfigItem() calls DecodeUTF() to convert the configuration file's
> text representation from, say, UTF-8 to dchar. So the call stack looks
> like this:
>
> LoadConfig
> ParseConfig
> ParseConfigItem
> DecodeUTF
>
> Now suppose the config file has some UTF encoding errors. This causes
> DecodeUTF to throw a DecodingError. ParseConfigItem can't go on, since
> that configuration item is mangled. So it propagates DecodingError to
> ParseConfig.
>
> Now, ParseConfig could simply abort, but using the idea of prebound
> handlers, it can actually offer two ways of recovering: (1)
> SkipConfigItem, to simply skip the mangled config item and process the
> rest of the config file as usual, or (2) ReparseConfigItem, to allow
> custom code to manually fix a bad config item and reprocess it.
>
> The problem is, ParseConfig doesn't know which action to take. It's too
> low-level to make that sort of decision. You need higher-level code,
> that knows what the application needs to do, to decide that. But
> ParseConfig can't just propagate the exception to said high-level code,
> because if it does, parsing of the entire config file is aborted and
> will have to be restarted from scratch.
>
> The solution is to have the higher-level code register a delegate with
> the exception system. Something like this:
>
> // NOTE: not real D code
> void main() {
> registerHandler(auto delegate(ParseError e) {
> if (can_repair_item(e.item)) {
> return e.ReparseConfigItem(
> repairConfigItem(e.item));
> } else {
> return e.SkipConfigItem();
> }
> });
>
> ParseConfig(configfile);
> }
>
> Now when ParseConfig encounters a problem, it signals a ParseError
> object with two options for recovery: ReparseConfigItem and
> SkipConfigItem. It doesn't try to fix the problem on its own, but it
> lets the delegate from main() make that decision. The runtime exception
> system then sees if there's a matching handler, and calls the handler
> with the ParseError to determine which course of action to take. If no
> handler is found, or the handler decides to abort, then ParseError is
> propagated to the caller with stack unwinding.
>
> So ParseConfig might look something like this:
>
> // NOTE: not real D code
> auto ParseConfig(...) {
> foreach (item; config_items) {
> try {
> // Note: not real proposed syntax, this is just
> // to show the semantics of the mechanism:
> restart:
> auto objs = ParseConfigItem(item);
> SetupConfigObjects(objs);
> } catch(ParseConfigItemError) {
> // Note: not real proposed syntax, this is just
> // to show the semantics of the mechanism:
> ConfigError e;
> e.ReparseConfigItem = void delegate(ConfigItem
> fixedItem)
> {
> goto restart;
> };
> e.SkipConfigItem = void delegate() {
> continue;
> }
>
> // This will unwind stack if no handler is
> // found, or handler decides to propagate
> // exception.
> handleError(e);
> }
> }
> }
>
> OK, so it looks real ugly right now. But if this mechanism is built into
> the language, we could have much better syntax, something like this:
>
> auto ParseConfig(...) {
> foreach (item; config_items) {
> try {
> auto objs = ParseConfigItem(item);
> SetupConfigObjects(objs);
> } recoverBy ReparseConfigItem(fixedItem) {
> item = fixedItem;
> restart; // restarts try{} block
> } recoverBy SkipConfigItem() {
> setDefaultConfigObjs();
> continue; // continues foreach loop
> }
> }
> }
>
> This is just a rough sketch syntax, just to show the idea. It can of
> course be improved upon.
>
>
> T
I was actually thinking something similar, the part about registering
exception handlers, i.e. using "registerHandler".
--
/Jacob Carlborg
More information about the Digitalmars-d
mailing list