-
Notifications
You must be signed in to change notification settings - Fork 20
Initial work on the error handling proposal #26
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| ## The `errors` pragma | ||
|
|
||
| The `errors` pragma is similar to the `raises` pragma, but it uses | ||
| the Nim type system to ensure that the raised recoverable errors | ||
| will be handled at the call-sites of the annotated function. | ||
|
|
||
| To achieve this, it performs the following simple transformation: | ||
|
|
||
| ```nim | ||
| proc foo(x: int, y: string): float {.errors: (ValueError, KeyError).} = | ||
| body | ||
| ``` | ||
|
|
||
| is re-written to the equivalent of: | ||
|
|
||
| ```nim | ||
| type | ||
| Raising[ErrorsList, ResultType] = distinct ResultType | ||
|
|
||
| proc foo_original(x: int, y: string): float {. | ||
| raises: [Defect, ValueError, KeyError] | ||
| .} = | ||
| body | ||
|
|
||
| template foo(x: int, y: string): untyped = | ||
| Raising[(ValueError, KeyError), float](foo_original(x, y)) | ||
| ``` | ||
|
|
||
| Please note that the original proc now features a `raises` annotation | ||
| that will guarantee that no other exceptions might be raised from it. | ||
| The `Defect` type was implicitly added to the list as a convenience. | ||
|
|
||
| The returned distinct type will be useless at the call-site unless | ||
| it is stripped-away through `raising`, `either` or `check` which are | ||
| the error-handling mechanisms provided by this library and discussed | ||
| further in this document. | ||
|
|
||
| If you accidentally forget to use one of the error-handling mechanisms, | ||
| you'll get a compilation error along these lines: | ||
|
|
||
| ``` | ||
| required type for x: float | ||
| but expression 'Raising[(ValueError, KeyError), float](foo_original(x, y))' is of type: Raising[tuple of (ValueError, KeyError), system.float] | ||
| ``` | ||
|
|
||
| Please note that if you have assigned the `Raising` result to a | ||
| variable, the compilation error might happen on a line where you | ||
| attempt to use that variable. To fix the error, please introduce | ||
| error handling as early as possible at the right call-site such | ||
| that no `Raising` variable is created at all. | ||
|
|
||
| `noerrors` is another pragma provided for convenience which is | ||
| equivalent to an empty `errors` pragma. The forced error handling | ||
| through the `Raising` type won't be applied. | ||
|
|
||
| Both pragmas can be combined with the `nodefects` pragma that | ||
| indicates that the specific proc should be proven to be Defect-free. | ||
|
|
||
| The transformation uses a template by default to promote efficiency, | ||
| but if you need to take the address the Raising proc, please add the | ||
| `addressable` pragma that will force the wrapper to be a regular proc. | ||
|
|
||
| Finally, `failing` is another pragma provided for convenience which | ||
| is equivalent to `{.errors: (CatchableError).}`. | ||
|
|
||
| ## The `raising` annotation | ||
|
|
||
| The `raising` annotation is the simplest form of error tracking | ||
| similar to the `try` annotation made popular by the [Midori error model](http://joeduffyblog.com/2016/02/07/the-error-model/), | ||
| which is also [proposed for addition in C++](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf) and [already available in Zig](https://ziglang.org/documentation/master/#try). | ||
|
|
||
| It merely marks the locations in the code where errors might be raised | ||
| and strips away the `Raising` type to disarm the compiler checks: | ||
|
|
||
| ```nim | ||
| proc attachValidator(node: BeaconNode, keyFile: string) {.failing.} = | ||
| node.addValidator(raising ValidatorPrivKey.init(raising readFile(keyFile)) | ||
| ``` | ||
|
|
||
| When applied to a `Result` or an `Option`, `raising` will use the | ||
| `tryGet` API to attempt to obtain the computed value. | ||
|
|
||
| ## The capital `Try` API | ||
|
|
||
| The capital `Try` API is similar to a regular `try` expression | ||
| or a `try` statement. The only difference is that you must provide | ||
| exception handlers for all possible recoverable errors. If you fail | ||
| to do so, the compiler will point out the line in the `try` block | ||
| where an unhandled exception might be raised: | ||
|
|
||
| ```nim | ||
| proc replaceValues(values: var openarray[string], | ||
| replacements: Table[MyEnum, string]) = | ||
| Try: | ||
| for v in mitems(values): | ||
| let | ||
| enumValue = parseEnum v | ||
| replacement = replacements[enumValue] # Error here | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is the hallmark of exception-unsafe code, and a school-book example of a design issue in The most "natural" thing to do if the compiler is pestering me about
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. notably, the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, this is just an illustrative example. After all, the code doesn't compile, so it being a "school-book example of a design issue" is reasonable. If you can offer a better example with exceptions that every Nim user will be familiar with, feel free to suggest it.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do you need to put
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You have to put it for APIs that use the |
||
| v = replacement | ||
| except ValueError: | ||
| echo "Invalid enum value" | ||
| ``` | ||
|
|
||
| The above example will fail to compile with an error indicating | ||
| that `replacements[enumValue]` may fail with an unhandled `KeyError`. | ||
|
|
||
| ## The `either` expression | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems like a small, out-of-scope utility - I wouldn't add until there's significant evidence that it's generally useful in real-world code - using a default after getting an error is a bit weird: the operation failed to calculate a value so now I use a some other value? why didn't the calculation give me that replacement value directly if that makes sense for the type? clearly, this is used when taking "shortcuts" with the type, and seems less legitimate
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would you censor a small utility like this? I find it useful, so let's see how much adoption it will get in practice. it's not just about providing a replacement value - it's also a control-flow construct.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. because it's meant to become a core concept in the language and imported in every module basically - you don't want cruft in there from the start - better start every feature at -100 points and work up a case for it, and in this case it's cheap and easy to do "manually" |
||
|
|
||
| The `either` expression can be used with APIs based on `Option[T]`, | ||
| `Result[T, E]` or the `errors` pragma when it's appropriate to | ||
| discriminate only between successful execution and any type of | ||
| failure. Regardless of the error handling scheme being used, | ||
| `either` is used like this: | ||
|
|
||
| ```nim | ||
| let x = either(foo(), fallbackValue) | ||
| ``` | ||
|
|
||
| On success, `either` returns the successfully computed value | ||
| and the failure side of the expression won't be evaluated at all. | ||
|
|
||
| Besides providing a substitute value, the failure side of the | ||
| expression may also feature a `noReturn` statement such as | ||
| `return`, `raise`, `quit` as long at it's used with the following | ||
| syntax: | ||
|
|
||
| ```nim | ||
| let x = either foo(): | ||
| return | ||
| ``` | ||
|
|
||
| Within the failure path, you can also use the `error` keyword to | ||
| refer to the raised exception or the `error` value of the failed | ||
| `Result`. | ||
|
|
||
| ## The `check` expression | ||
|
|
||
| The `check` macro provides a general mechanism for handling | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so we don't call it
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| the failures of APIs based the `errors` pragma or `Result[T, E]` | ||
| where `E` is an `enum` or a case object type. | ||
|
|
||
| It takes an expression that might fail in multiple ways together | ||
| with a block of error handlers that will be executed in case of | ||
| failure. If the user failed to cover any of the possible failure | ||
| types, this will result in a compilation error. | ||
|
|
||
| On success, the `check` returns the successfully computed value | ||
| of the checked expression. In case of failure, the appropriate | ||
| error handler is executed. It may produce a substitute value or | ||
| it may return from the current function with a `return`, `raise`, | ||
| `quit` or any other `noReturn` API. | ||
|
|
||
| The syntax of the `check` expression is the following: | ||
|
|
||
| ```nim | ||
| let x = check foo(): | ||
| SomeError as err: defaultValue | ||
| AnotherError: return | ||
| _: raise | ||
| ``` | ||
|
|
||
| If the `foo()` function was using the `errors` pragma, the | ||
| above example will be re-written to: | ||
|
|
||
| ```nim | ||
| let x = try: | ||
| raising foo() | ||
| except SomeError as err: | ||
| defaultValue | ||
| except AnotherError: | ||
| return | ||
| except CatchableError: | ||
| raise | ||
| ``` | ||
|
|
||
| Alternatively, if `foo()` was returning a `Result[T, E: enum]`, the | ||
| example will be re-written to: | ||
|
|
||
| ```nim | ||
| let x = foo() | ||
| if x.isOk: | ||
| x.get | ||
| else: | ||
| case x.orror: | ||
| of SomeError: | ||
| let err = x.error | ||
| defaultValue | ||
| of AnotherError: | ||
| return | ||
| else: | ||
| raiseResultError x | ||
| ``` | ||
|
|
||
| The `Result` error type can also be a case object with a single `enum` | ||
| discriminator that will be considered the error type. The generated code | ||
| will be quite similar to the example above. | ||
|
|
||
| Please note that the special default case `_` is considered equivalent | ||
| to `CatchableError` or `else` when working with enums. | ||
|
|
||
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 unification with
Result, this is practically the same as?- should?be smarter and convert back-and-forth between the error handling models? from a performance POV, that's a bit dumb (the visual cost is much smaller than the incurred penalty) but it's... pragmatic.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 considered this as well, but it may be indeed too magical. I guess we'll know soon with more practice.