February 21, 2012
On 2012-02-21 21:27, Andrei Alexandrescu wrote:
> On 2/21/12 2:26 PM, Jacob Carlborg wrote:
>> As I said, it seems you want to push up implementation details specific
>> to a given subclass to the base class even though it shouldn't be pushed
>> up.
>
> I explained that doing so allows for proper formatting of error
> messages. So it should pushed up.
>
> Andrei

Well, I don't think that is the right approach. As many others have explained, error messages are only a small part of exception handling.

If you do want to have a generic way of getting an error message out of an exception, what's wrong with toString? Or a new method that formats the error messages. No need to push up the instance variables to the base class.

-- 
/Jacob Carlborg
February 21, 2012
On Tue, Feb 21, 2012 at 09:32:35PM +0100, deadalnix wrote: [...]
> So it doesn't help. Dulb subclasses of Exceptions are done mostly to be able to catch them. To avoid useless subclasses, we need a more precise way to catch Exception than the type only.
[...]

This is a good point.

Has anybody even considered, *why* does catch match by type? Is there a good reason for that? Or is it just inherited from the rah rah days of OOP where "everything is an object", so since exception is part of everything, exception is an object, therefore we can just catch by object type?

>From all the heated debate in this thread, it's clear that exceptions
don't map very well to a class hierarchy, at least not without points of contention and what amounts to workarounds and hacks.

So I'm going to throw (har har) this very crazy and wild idea out there
and let's see if it's viable:

What if instead of catching by class, we catch by attribute matching? So instead of writing:

	try { ... }
	catch(SomeExceptionType e) { ... }
	catch(SomeOtherExceptionType e) { ... }
	catch(YetAnotherSillyException e) { ... }

we write:

	try { ... }
	catch(e: exists!e.filename && e.failedOp is File.open) {
		// something
	}
	catch(e: e.is_transient && e.num_retries < 5) {
		// something else
	}
	// And why should we even need an exception object in the first
	// place?
	catch(time() >= oldtime+5000) {
		// This thing's been running for way too long, time to
		// do something drastic
	}

Flamesuit on! ;-)


T

-- 
Famous last words: I *think* this will work...
February 21, 2012
Le 21/02/2012 01:38, Andrei Alexandrescu a écrit :
> On 2/20/12 6:25 PM, H. S. Teoh wrote:
>> On Mon, Feb 20, 2012 at 05:15:17PM -0600, Andrei Alexandrescu wrote:
>> Formatting should use class reflection. We already discussed that, and
>> we already agreed that was the superior approach.
>
> Jose's argument convinced me otherwise. I retract my agreement.
>
>> When you're catching a specific exception, you're catching it with the
>> view that it will contain precisely information X, Y, Z that you need to
>> recover from the problem. If you don't need to catch something, then
>> don't put the catch block there.
>
> That's extremely rare in my experience, and only present in toy examples
> that contain a ton of "..." magic.
>

I think you experience here is biased by C++ .
February 21, 2012
On 2/21/12 2:42 PM, Jacob Carlborg wrote:
> On 2012-02-21 21:27, Andrei Alexandrescu wrote:
>> On 2/21/12 2:26 PM, Jacob Carlborg wrote:
>>> As I said, it seems you want to push up implementation details specific
>>> to a given subclass to the base class even though it shouldn't be pushed
>>> up.
>>
>> I explained that doing so allows for proper formatting of error
>> messages. So it should pushed up.
>>
>> Andrei
>
> Well, I don't think that is the right approach. As many others have
> explained, error messages are only a small part of exception handling.

I agree. Also, one interface function is only a small part of a class hierarchy.

> If you do want to have a generic way of getting an error message out of
> an exception, what's wrong with toString? Or a new method that formats
> the error messages. No need to push up the instance variables to the
> base class.

This has been answered in the long thread. In brief, toString loses too much information and putting formatting inside exceptions is not the right place.


Andrei


February 21, 2012
On 2/21/12 2:47 PM, H. S. Teoh wrote:
> On Tue, Feb 21, 2012 at 09:32:35PM +0100, deadalnix wrote:
> [...]
>> So it doesn't help. Dulb subclasses of Exceptions are done mostly to
>> be able to catch them. To avoid useless subclasses, we need a more
>> precise way to catch Exception than the type only.
> [...]
>
> This is a good point.
>
> Has anybody even considered, *why* does catch match by type? Is there a
> good reason for that? Or is it just inherited from the rah rah days of
> OOP where "everything is an object", so since exception is part of
> everything, exception is an object, therefore we can just catch by
> object type?

I think a reason is that exceptions should be able to transport an arbitrary amount of information. That means heterogeneous types at least. Then, as you discussed in the beginning of the thread, placing types in a hierarchy allows client code to catch several exceptions sharing a common super type, theory that was well understood.

A more debatable aspect of exceptions is the first-match rule in catch blocks. All of OOP goes with best match, except here. But then all code is together so the effect is small.

>> From all the heated debate in this thread, it's clear that exceptions
> don't map very well to a class hierarchy, at least not without points of
> contention and what amounts to workarounds and hacks.
>
> So I'm going to throw (har har) this very crazy and wild idea out there
> and let's see if it's viable:
>
> What if instead of catching by class, we catch by attribute matching?
> So instead of writing:
>
> 	try { ... }
> 	catch(SomeExceptionType e) { ... }
> 	catch(SomeOtherExceptionType e) { ... }
> 	catch(YetAnotherSillyException e) { ... }
>
> we write:
>
> 	try { ... }
> 	catch(e: exists!e.filename&&  e.failedOp is File.open) {
> 		// something
> 	}
> 	catch(e: e.is_transient&&  e.num_retries<  5) {
> 		// something else
> 	}
> 	// And why should we even need an exception object in the first
> 	// place?
> 	catch(time()>= oldtime+5000) {
> 		// This thing's been running for way too long, time to
> 		// do something drastic
> 	}
>
> Flamesuit on! ;-)

The only problem I see here is ascribing e a type.


Andrei
February 21, 2012
On Tue, Feb 21, 2012 at 02:40:30PM -0500, Jonathan M Davis wrote:
> On Tuesday, February 21, 2012 00:15:48 H. S. Teoh wrote:
> > TRANSITIVITY
> 
> I still contend that this useless, because you need to know what went wrong to know whether you actually want to retry anything. And just because the particular operation that threw could be retried again doesn't mean that the code that catches the exception can retry the function that _it_ called which resulted in an exception somewhere down the call stack. And often, you don't even know which function it was that was called within the try block. So, I don't see how transivity really matters. If you know what what wrong - which the type of the exception will tell you - then _that_ is what helps your code make a useful decision as to what to do, not a transivity property.

The point of my little exercise was not to try to solve *every* case of exception handling, but to isolate those cases for which generic handling *does* make sense. I don't pretend that my categories cover *every* case. They obviously don't, as you point out.  For those cases where it doesn't make sense, you still catch the specific exception and do your specific recovery, as before.

What I'm trying to do is to evaluate the possibility/feasibility of an error recovery system where you *don't* have to know what the specific error is, in order to do something useful with it. What are the errors that *can* be recovered this way, if there are such errors.

This is why transitivity is useful, because it allows you to not have to worry about what the specific problem is, and yet still be able to do something meaningful, because the problem doesn't change in nature as you move up the call stack. Obviously, non-transitive errors have to be handled on a case-by-case basis; I'm not negating that at all.

The importance of transitivity to generic handling can be seen as follows:

Suppose X calls Y and Y calls Z. Z encounters a problem of some sort, let's say for instance it's an input error. So from Y's point of view, the problem can be corrected if it passes different input to Z.  But from X's point of view, this is not necessarily true: X's input to Y may have nothing to do with Y's input to Z. So trying to generically handle Z's problem at X's level makes no sense. The problem is not transitive, so no generic handling is possible. You have to catch by type and recover by type. End of story.

But suppose Z encounters a transitive problem. Say for instance it's a component failure: one of the functions that Z calls has failed. Well, by extension, that also means Z itself has failed. Now, from Y's point of view, it can attempt to recover by calling W in lieu of Z, if there exists a W that performs an equivalent function to Z. But since the problem is transitive, we can go up to X's level. From X's point of view, it knows nothing about Z, but it *does* know that Y has encountered a component failure (since component failure is transitive). Viewed from X's perspective, Y is the problem (it doesn't know about Z). So if there's an alternative to Y that performs an equivalent function, X can call that in lieu of Y.

So you see, even though X has absolute no idea what kind of problem Z encountered, or even that such a thing as Z exists, it *can* make a meaningful effort to recover from the problem that Z encountered. That's what I'm trying to get at: generic handling.

Now, to avoid misunderstandings, I'm *not* saying that at every level of the call stack there needs to be a recovery mechanism, such that Y has an alternative to Z and X has an alternative to Y, and so on, all the way up the stack. That would be foolish, since you'll be wasting time writing alternative versions of everything.

How this approach benefits real-life programs is that you can insert recovery mechanisms at strategic points in the call chain, so that when problems occur lower down the stack, you can handle it at those points without needing to unwind the stack all the way to the top. Obviously, the try-catch mechanism already does this; but the difference is that those strategic points may not be high enough up the call chain to be able to make a decision about *which* recovery approach to take. They know how to implement the recovery, but they don't know if they should try, or just give up. So this is here is where the high-level delegates come in. *They* know how to decide whether to attempt recovery or just give up, but they *don't* know how to implement recovery, because the lower-level code is a black box to them.

By tying the two together via the "Lispian system", you have the possibility of making decisions high up the call stack, and yet still be able to effect low-level recovery strategies.

To go back to the earlier example: if Z fails, then from X's point of view Y has also failed, since it doesn't know what Z is. However, if X registers a component failure handler and then calls Y, and Y is capable of switching to W instead of Z, then when Z fails, X's delegate gets to say "Oh, we have a component failure somewhere in Y. Let's try to replace that component" -- even though X has no idea how to do this, nor even what that failed component was. But Y does, so after X's delegate says "try to replace the failed component", Y swaps in W, and continues doing what it was supposed to.

If you still doubt the usefulness of such a system, consider this: X implements a complex numerical computation, among the many operations of which includes solving a linear system, which is done by Y. To solve the system, Y at some point calls Z, which, say, inverts a matrix. But due to the particular algorithm that Z uses for matrix inversion, it runs into a numerical overflow, so it throws an error. Y catches the error, and informs X's delegate, "hey, I've encountered a problem while solving your linear system, but I know of another algorithm that I can try that may give you the result you want; should I proceed?". X's delegate then can decide to give up - "No, this is a real-time system, and there's not enough time left to attempt another calculation", or it can decide to go ahead "I want that system solved, no matter what!". If X's delegate decides to continue, Y will swap in an alternative way of inverting the matrix that avoids overflow, and thus be able to finish its computation.

If there was no way for Y to talk to X (via X's delegate) while control is still in the context of Y, then Y would be forced to make a decision it's not qualified to make (it may be just a generic algebraic package, it doesn't know it's running in a real-time environment or not), or it has to unwind the stack back to X, at which point it's too late for X to salvage the situation (it has to call Y all over again if it wants to re-attempt the computation -- and even then there may not be a way for X to tell Y to replace Z with W, because X doesn't even know what Z and W are).


> So, yes. A particular exception type is generally transitive or not, but you know that by the nature of the type and the problem that it represents. I don't see how focusing on transivity is useful.
[...]

It's funny, earlier in this thread I argued the same thing about Andrei's is_transient proposal.

But I've since come to realize what Andrei was trying to get at: yes we know how to deal with specific errors in a specific way, that's given; but *are* there cases for which we don't *need* to know the specifics? Can we take advantage of generic handling for those cases, so as to reduce (not eliminate!) the amount of specific handling we need to do?

I think that angle is worth pursuing, even if it's to conclude at the end that, no, generic handling is not possible/feasible. OK, lesson learned. We move on.

But if there are cases for which generic handling *is* possible, and if we can implement powerful error recovery schemes in a generic way, then we'd be foolish not to take advantage of it.


[...]
> So, I see no problem with you experimenting, but I don't think that we should be drastically changing how the standard library functions with regards to exceptions. We would definitely gain something by cleaning up how they're organized, and maybe adding additional capabilites to improve their printing abilities like Andrei wants to do would be of some value, but we don't need a complete redesign, just some tweaks.
[...]

Currently deadalnix & myself are experimenting with ways of implementing the "Lispian system" as a separate Phobos module that offers enhanced exception handling, on top of the usual try/catch mechanism. I'm not proposing to replace anything, at least not in the short term. Should this system prove worthwhile in the long run, *then* we can start thinking about whether we can make extensive use of it in the library code, or reworking exception handling at the language level.


T

-- 
Two wrongs don't make a right; but three rights do make a left...
February 21, 2012
On Tue, Feb 21, 2012 at 03:15:19PM -0600, Andrei Alexandrescu wrote: [...]
> A more debatable aspect of exceptions is the first-match rule in catch blocks. All of OOP goes with best match, except here. But then all code is together so the effect is small.

Does it make sense to make it best-match? Or is that too risky since everyone expects it to be first-match?


[...]
> >So I'm going to throw (har har) this very crazy and wild idea out
> >there and let's see if it's viable:
> >
> >What if instead of catching by class, we catch by attribute matching? So instead of writing:
> >
> >	try { ... }
> >	catch(SomeExceptionType e) { ... }
> >	catch(SomeOtherExceptionType e) { ... }
> >	catch(YetAnotherSillyException e) { ... }
> >
> >we write:
> >
> >	try { ... }
> >	catch(e: exists!e.filename&&  e.failedOp is File.open) {
> >		// something
> >	}
> >	catch(e: e.is_transient&&  e.num_retries<  5) {
> >		// something else
> >	}
> >	// And why should we even need an exception object in the first
> >	// place?
> >	catch(time()>= oldtime+5000) {
> >		// This thing's been running for way too long, time to
> >		// do something drastic
> >	}
> >
> >Flamesuit on! ;-)
> 
> The only problem I see here is ascribing e a type.
[...]

True, for this to work in its full generality would require duck-typing (the catch block can use any property it tests for, regardless of type). Which D doesn't have.

So it looks like we're back to catch conditions, that has come up a few times in this thread:

	try { ... }
	catch(Exception e: e.is_transient && ... || ...) {
		// or whatever the latest proposed syntax is, the idea
		// is the same.
	}
	catch(Exception e: !e.is_transient && ...) {
		// the previous block doesn't catch if conditions fail,
		// so we can still get Exception here.
	}

This does start to look like it might make sense to switch to best-match instead of first-match for catch clauses.


T

-- 
Your inconsistency is the only consistent thing about you! -- KD
February 21, 2012
On 02/18/2012 09:09 PM, Jim Hewes wrote:

> I think of exception handling as tied to contract programming.

I think your use of the word 'contract' is colliding with the contract programming feature. What you describe later does not match with the contract programming and I guess is the reason why Andrei is pointing out two chapters from TDPL.

I will reread those chapters later today but I think Andrei is referring to the distinction between assert() and std.exception.enforce().

> A
> function has a specific job that it's supposed to do. If for any reason
> it cannot do that job successfully, an exception should be thrown.

Agreed. That is how we have been using exceptions in C++ successfully at where I work. It makes everything simple.

> That
> can include even bad parameters

Yes, enforce() in D is great for that. I think Andrei agrees.

> (although if you have bad parameters to
> internal functions I'd think that is a design bug and could be handled
> by asserts).

Yes. Contract programming uses asserts().

> If what you mean is that exceptions should not be used to return
> information when the function is successful, I agree. But it can be used
> to return extra details about errors when a function fails.

Agreed. Again, this is how we have been using exceptions. We have the equivalents of detailed exception types that Jonathan M Davis has been mentioning. Come to think of it, less than a dozen.

> Not every
> exception coming out of a function was generated by that function. It
> may have come from several levels below that.

Of course. It is very useful.

> Maybe you can handle the
> former because it is more immediate but not the latter. So without
> exception types how would you know the difference between them? You
> could use error codes and switch on them but it defeats one of the main
> purposes of exception handling. So I think if there are exceptions
> generated from further away, it's an argument _for_ exception types
> rather than against it.

Agreed.

> For example, just have one BadParameter exception and then store
> information about which parameter is bad and why in the exception.

Agreed.

I would like to add that exceptions are thrown by code that doesn't know how the exception will be useful. For that reason, especially a library function must provide as much information as possible. Going with the same examples in this thread, FileException is not a good exception to throw when the actual problem is a FilePermissionException or a FileNotFoundException.

Ali

February 21, 2012
Le 21/02/2012 22:15, Andrei Alexandrescu a écrit :
>> What if instead of catching by class, we catch by attribute matching?
>> So instead of writing:
>>
>> try { ... }
>> catch(SomeExceptionType e) { ... }
>> catch(SomeOtherExceptionType e) { ... }
>> catch(YetAnotherSillyException e) { ... }
>>
>> we write:
>>
>> try { ... }
>> catch(e: exists!e.filename&& e.failedOp is File.open) {
>> // something
>> }
>> catch(e: e.is_transient&& e.num_retries< 5) {
>> // something else
>> }
>> // And why should we even need an exception object in the first
>> // place?
>> catch(time()>= oldtime+5000) {
>> // This thing's been running for way too long, time to
>> // do something drastic
>> }
>>
>> Flamesuit on! ;-)
>
> The only problem I see here is ascribing e a type.
>

That is why I proposed here : http://forum.dlang.org/thread/jhos0l$102t$1@digitalmars.com?page=25#post-jhtus4:2416tp:241:40digitalmars.com some alternative syntax. To make it short :

try {
    // Stuff . . .
} catch(Exception e) if(e.transient == false) {
    // Handling . . .
}

For the longer explanation, see the link.
February 21, 2012
On Tue, Feb 21, 2012 at 07:15:29PM +0100, Artur Skawina wrote:
> On 02/21/12 17:56, H. S. Teoh wrote:
[...]
> I don't think something like this can reliably work - handling unknown error conditions in code not expecting them is not a good idea.

I'm not proposing we do this for *every* error. Only for those for which it makes sense. And the code *does* have to expect what it's handling: a particular category of problems, a category for which it makes sense to handle recovery in a generic way. I'm *not* proposing this:

	try {...}
	catch(Exception e) {	// catch everything
		// generic code that magically handles everything
	}

If we could do that, this thread would've been over after 3 posts. :)


> After all, if the new error is of a similar nature as another one it could have been mapped to that one, or handled internally.

Which is what I'm trying to do with the categorization.

Internal handling, of course, can and should be done in those cases where it's crystal clear how one should proceed. I'm addressing the case where it can't be handled internally (else why would it throw an exception / raise a condition in the first place?)


> Note that with my scheme the delegates can eg call another delegate provided in the exception from the lower level code - so things like that are possible. It's just that i don't think it's a good idea for low level code to use the exception mechanism to ask "Should I retry this operation?". The code either knows what to do (whether retrying makes sense) or could be provided with a predefined policy.

The delegate is the predefined policy. Except that it's much more flexible than a global on/off setting. It has access to the registering function's scope, based on which it can make decisions based on the large-scale state of the program, which the low-level code doesn't (and shouldn't) know about. Multiple delegates can be registered for the same condition, and the most relevant one (the closest to the problem locus) takes priority. Each delegate can implement its own policy on how to deal with problems. The higher-level delegates only see problems that the lower-level delegates decide to pass on.

In this way, lower-level delegates filter out stuff that they can handle on their own, only deferring to the higher-level delegates when they don't know how to proceed.  Your higher-level handler won't be handling all sorts of trivial low-level problems, only the major issues that the lower-level handlers can't already handle.


> If retrying occurs often during normal operation then throwing an exception every time is probably not the best way to handle this.

In fact, some experimental code that deadalnix & I are playing with currently implements retry without try/catch, except when we need to unwind the stack. So no performance hit there.


> And if it's a rare event - this kind of error handling adds too much overhead - programmer-wise, hence more bugs in the rarely executed parts of the program and probably java-style catch-everything-just-to-silence- -the-compiler situations, which are then never properly fixed...
[...]

We're currently playing with using templates to generate the boilerplate stuff, so all you need to do is to write the template name and wrap a block around the code to be retried. I think that's acceptable overhead -- it's no worse than writing "try ... catch", and arguably far more powerful.


T

-- 
Trying to define yourself is like trying to bite your own teeth. -- Alan Watts