-
-
Notifications
You must be signed in to change notification settings - Fork 48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Stateful exceptions #76
base: main
Are you sure you want to change the base?
Conversation
We currently have two very distinct idioms to handle errors in the language and standard library. | ||
|
||
1. Exceptions. They are used most of the time but since they are valueless, they can't be used to propagate the reason of an error to a caller function. | ||
2. Union types of the normal result and the error reason(s). This is used when the reason of an error is needed by a caller function (e.g. the constructors of `File` in the standard library). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For completeness, the third idiom would be having a notifier object that will be invoked for any errors encountered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add that.
I'm not a big fan of using Note that choosing something like However, I'd like to present a different idea for how to handle the error value that I think will be less magic and more useful. try
risky_operation()
elsematch
| FooError => Debug("Whoops, a foo error occurred")
| BarError => Debug("Sorry, a bar error occurred")
end This comes partly from the observation that almost every useful code snippet that used try
risky_operation()
else
match current_error
| FooError => Debug("Whoops, a foo error occurred")
| BarError => Debug("Sorry, a bar error occurred")
end
end Condensing the operation into An alternative name for the keyword could be I also see a need for another convenient shortcut in this process - many try
risky_operation()
elsematch
| FooError => Debug("Whoops, a foo error occurred")
| BarError => Debug("Sorry, a bar error occurred")
| let e: Any val => error e
end However, we could make this more convenient and more obvious to the reader using another keyword: try
risky_operation()
elsematch
| FooError => Debug("Whoops, a foo error occurred")
| BarError => Debug("Sorry, a bar error occurred")
elseraise // let any other error value get raised out to the next `try` block
end Overall, this style of error handling should be quite familiar to programmers familiar with other exception-oriented languages, in that it makes pattern matching on the error type / value a first-class part of the block where the error is caught. There are probably a few languages that use the "special context-sensitive value named by a reserved identifier", but Ruby is the only one that comes to mind for me, and in Ruby, using the "special identifier" is a code smell - the generally accepted convention is to prefer the syntax that matches a type and catches it in your own identifier ( |
As a smaller comment - In the Pony Patterns entry on this, I think we should recommend some basic conventions for error values. I think it's worth brainstorming a bit in this RFC to talk about what those conventions could be. I can think of two basic useful conventions:
For
This will give a convention of always having the location that raised the error (though not the full backtrace) be available as part of the error value, which should be convenient for debugging. |
@jemc I really like your Also, I agree with the conventions you're proposing. |
- `elsematch` semantics - Conventions for the Pony Pattern
RFC and implementation updated. |
This RFC seems rather problematic to me. Typed/valued exceptions have significant problems, and their evolution in Java, C#, C++, etc, should be carefully examined. Some major issues:
Unless a proposal can be made that simultaneously handles:
...I would strongly oppose this change. I'm very willing to be convinced otherwise, but this is a very serious and deep area, and would need an extensive evaluation (including, I think, a fully defined operational semantics, in the mathematical sense) before it could be considered. |
@sylvanc I've thought about the concerns you've raised and I may have an idea to mitigate them with a few changes to the RFC and good interface design conventions. I'll experiment a bit and report back. |
@sylvanc and I also ended up discussing this over the weekend @Praetonus. @sylvanc can you capture some of what we discussed here? |
We looked at how Common Lisp and Dylan signal conditions (the equivalent of an exception, but allowing local rather than non-local signal handlers - the equivalent of catching an exception). This seems similar to ideas @jemc put forward elsewhere about passing a lambda into a call as the error handler. |
Just want to add a link to an article with an overview of different error handling approaches http://joeduffyblog.com/2016/02/07/the-error-model/ and a good description of checked exceptions model. |
@prepor Thanks for the link, I'll take a look. |
Reading through @prepor's great link on error handling systems really reinforced for me the general impression I got when reading this RFC that I disagreed with one of the points in the Motivation section.
I agree that it can cause some confusion about what to use in a particular situation, but I think that all of the current error handling patterns we have in Pony are useful and appropriate in different situations. One of the big takeaways for me from @prepor's link is that we definitely need to keep in mind that many of the different cases where someone might reach for an exception (in say, Java) actually do fit most appropriately into different patterns for how they are best handled. One other takeaway is that our (not yet fully developed) "notifier" pattern that I insisted we mention as point 3 seems very similar to the "keeper" pattern discussed by the author in Midori. We should probably spend some time/effort trying to understand how Midori used "keepers" and how they might be applied in Pony. I really think that coming up with good patterns in that direction will bring great improvement to the Pony ecosystem in the long run. |
I've updated the RFC. @sylvanc I came up with a system with exception specifications. While it's one of the things you're concerned about, I think it can be mostly solved with good API design, as I explain in details in the updated RFC. @jemc Midori's keeper pattern is certainly interesting. I've added some thoughts about it in the RFC. The updated implementation, with modifications to the |
I think the latest changes seem very similar to the Midori approach from @prepor's link. I'm glad to see that the changes incorporated the idea of promoting the "one way to fail" paradigm as the default best practice in literature and throughout the standard library, with checked exceptions being the "exception rather than the rule" (if you'll permit the pun). I definitely do think that checked exceptions could be useful in some limited cases, where the API really benefits from them and where the caller is expected to deal with them very close to the call site. Personally, I'm inclined to be favorable toward this RFC, provided that we can really put a lot of emphasis on teaching users that typed exceptions should be a last resort, with "one way to fail" being considered the norm. |
I forgot to mention a change about |
This was discussed on the sync call yesterday. Several ideas came up, namely using exceptions as a local return. I'll experiment with that and report back. |
Would my 2c be welcome in this discussion, given that I'm a complete outsider and barely know pony? |
@micklat We welcome all constructive input. You're being an "outsider" doesn't really matter. The "barely know Pony" might have some impact but that is more context. This could be a good way to become more engaged with the community, be less of an "outsider" and learn more about Pony. |
Thanks for the invitation. I meant to suggest inferring the exception annotations with an algorithm similar to let-polymorphism. I thought this would largely solve the accidental coupling between the thrower of exceptions and the functions that the exception "passes through" on its way to the handler. But I now realize that I don't understand this problem well enough to provide a detailed proposal, so I'll pass. Especially, I don't know how that would work with traits and interfaces - whether let-polymorphism can accommodate those or not. If it could work, though, that'd be great, because you'd get the benefits of checked exceptions without the downsides. |
@micklat There are many ways to participate, can you provide some links to reading material that folks could peruse about let-polymorphism? Perhaps we can help provide some of that context for you. |
I can provide some links, yes. The best source I know is Benjamin Pierce's "Types for programming languages", from which I've studied long ago - chapter 22 presents let-polymorphism (section 7). A much shorter and freely-available source is Wikipedia's, "Hindley–Milner type system". I have not read it in full, but it seems to cover the material I know while introducing the necessary notation. My previous comment may give the impression that inferring exception types is a simple matter. I suspect that it is, but type theory has a way of surprising naive observers such as myself. There is quite likely a catch somewhere of which I am not aware. Alas, I don't have the time to work it out, so all of this may be just noise. Nonetheless, if anybody reading this is not aware of let-polymorphism, then perhaps they shall benefit from the introduction. It is the magic that allows languages such as ML to have generic types without requiring type annotations. This seems to me to be analogous to having functions that are generic with respect to the exceptions that their callees may throw, without requiring this genericity to be stated explicitly. Alas, a devil may lurk in the details. Perhaps silvanc knows something about that. |
I'll also mention that Xavier Leroy has published work on the topic of inferring exception types: http://gallium.inria.fr/~xleroy/bibrefs/Pessaux-Leroy-exn.html I took a peek at this approach and it didn't seem very simple to me. This suggests either that the problem is harder than I thought, or that Leroy was committed to the previous design choices of Ocaml which somehow prevented a simpler solution. |
Hi, I am novice to pony, so please take my comment with a grain of salt. First I write about my past experiences based on Ruby and Go and then I tell you about my own idea. Experience with raising exceptions (Ruby)cons:
Experience with passing errors (Go)pros:
cons:
Own ideaThis builds on the Go way, but without the code repetition and with stack traces. Further, I think error handling in general boils down to either handling an error But instead of returning error values, let's invert the process: The function that would have returned an error value (in the Go code) now passes it to the given error-handler. The error-handler would also be a built in interface (like error), with a method that is called with the argument of the error value. A built in universal error-handler is passed to the entry point of the program (Main.create) by the runtime. It then can by passed all the way down as "last resort"/fall-back. In the cases, where the errors (that can't be handled locally) should not propagate up to Main.create, one can define own types of error handlers; they may
pros:
In summary it is related to the lambda idea, but with an interface instead of a lambda and with the benefits of (1) having multiple possible implementations of the interface, (2) one global fall-back and (3) a nicer type signature. It may be that in the case of pony (with actors) this makes no sense - I'm not sure. Just wanted to share the idea. |
Rendered.