Replies: 1 comment
-
|
I'm going through and cleaning up old emails and Slack threads, and had a reply to this that I don't want to lose -- the "rethrowing a nested error" scenario (via It's interesting to me that this |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
(migrated from https://github.com/orgs/brownplt/teams/pyret/discussions/11)
This is a proposal for a way to handle erroneous execution in Pyret that emphasizes making "throwing" an error lightweight, while still handling them in a typable way without
catch. It was languishing in a Slack post, and I figure it's better to have it in a more central place. I don't expect to move on this any time soon.A New Control-Flow Construct
Pyret gets a new keyword, failwith, which is part of a function declaration:
failwithbinds a continuation at the point of the function start for reporting errors. In the example above, this is the identifierfail.failaccepts arguments of typeErrorin this example, which is a built-in datatype in Pyret. Calling thefailfunction the first time in the dynamic extent of the call tostring-to-numberjumps back to the start of thestring-to-numberfunction with the error value. Calling a particular binding offailmore than once, or outside the extent ofstring-to-number, is a bug, and causes an uncatchable error. That is,failis an escape continuation.Strawman alternative: Pyret could have a new keyword that is a local control-flow construct. For example, we could call it
local-throw, and it could jump to the boundary of the enclosing function. However, Pyret has so many functions defined for things likeforand local helpers that a useful meaning of "the nearest enclosing function" is tricky to pin down (a similar problem arises withthisin JavaScript). We know enough about continuations to bind them as identifiers and allow helper functions, etc, to trigger the failure.Handling Errors
With no other changes, this would merely act as a new binding position for the existing
raisefunction. A caller ofstring-to-numberhas no special way to catch the failure, so this call would act just as it does in current Pyret, terminating the whole computation if it fails:Catching Errors
To handle errors, Pyret gets a new syntactic form for calling functions, which will evaluate to an
Eitherof the successful answer or the error value. To use it, append?to the function name:Note that a
?-call works naturally on functions that aren't defined with afailwithclause. A function without afailwithclause behaves the same in a?expression as a function with afailwithclause that never signals an error.Local Handling Only
This new
?calling form only catches errors raised directly by the failure handler of the function being called, not any other failures. This means that if the author of a function didn't consider catching an error or raising it explicitly, the caller cannot choose to notice and handle those errors. For example, if we write a function that callsstring-to-numberbut doesn't handle invalid input errors, the function always errors, no matter how it is called:This local handling rule is a design decision that deliberately weakens the extent of error handling:
It encourages catching errors close to where they happen, since they can only be handled immediately at the call that may error.
It correctly ascribes blame to the particular function invocation that caused the error, as opposed to an exception handler that could have received an exception from one of many locations in the program.
It makes type-checking simpler, since a call site only needs to know the declared
failwithtype in order to type-check a?expression to the correct Either type.Type-checking
This adds a new component to function types, which is the failwith position:
Subtyping the failwith position is covariant, just like the return position.
A function that doesn't declare a failwith position has Bot as its failwith position:
Function annotations written without a
?position are sugar for Top in that position:is sugar for
(Note that this may be one level more subtle – it might be that positive
failwithpositions get Bot and negativefailwithpositions get Top, to handle nested contravariant positions in annotations. This would need discussion on details and soundness.)This version of
mapaccepts a function with any error declaration, due to subtyping, and with the default implementation ofmap, would catch no errors and throw whatever underlying errors the callback throws.Some Examples
Deferring errors in map
Note that we could also write map as:
Then
mapcould be called with?and return the (first) failure. Since theEithernonsense really pollutes the function, we could go one further. The unwrapping and forwarding logic could be moved into a helper:Helpers like
defer-errorsNcan be builtin for this kind of situation. This lets a function choose naturally if it wants to handle errors from a callback, return the same catchable errors as the callback, or if a callback error should simply terminate the whole program (e.g. it's a bug).mapcould also choose to not terminate on the first error, and instead have its fail take aList<c>, reporting all the errors that occurred.This proposal doesn't come with a clear sense of what the "right" version of
mapis, though I suspect the one above is pretty good. The strength of the proposal is that it lets us experiment with different patterns, and come to a set of best practices for situations likemapandfilter. The weakness is that it's not clear the default is the right thing.Files, Script vs Program
A quick script to do some work:
A more principled program fragment:
Implementation and Performance
Escape continuations don't cause any stack-copying, so in both a more traditional implementation and in the JavaScript compiler, we simply need to leave enough information on the stack to "jump" it back to the right place, and to mark fail continuations as no longer valid (in case they end up stored on the heap and called later).
In an assembly implementation, I'd change the calling convention to have success and failure continuations as stack/code pointers explicitly in the call. A
?call would point to code that allocates a right with the success and a left with the failure, and the return of the function would jump to the right place. A normal call would simply have a failure continuation that aborts the computation as normal.Allocating the functions to do this in JavaScript is probably a non-starter, so we probably want
failto raise a special kind of exception that is caught by?call sites and passes through other handlers. Then the catch of that special handler can allocate the left before using the result.There are clear opportunities for inlining and optimizing
?calls that appear directly inEithercase matches in the future.Beta Was this translation helpful? Give feedback.
All reactions