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.
-
(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:
failwith
binds a continuation at the point of the function start for reporting errors. In the example above, this is the identifierfail
.fail
accepts arguments of typeError
in this example, which is a built-in datatype in Pyret. Calling thefail
function the first time in the dynamic extent of the call tostring-to-number
jumps back to the start of thestring-to-number
function with the error value. Calling a particular binding offail
more than once, or outside the extent ofstring-to-number
, is a bug, and causes an uncatchable error. That is,fail
is 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 likefor
and local helpers that a useful meaning of "the nearest enclosing function" is tricky to pin down (a similar problem arises withthis
in 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
raise
function. A caller ofstring-to-number
has 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
Either
of 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 afailwith
clause. A function without afailwith
clause behaves the same in a?
expression as a function with afailwith
clause 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-number
but 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
failwith
type 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
failwith
positions get Bot and negativefailwith
positions get Top, to handle nested contravariant positions in annotations. This would need discussion on details and soundness.)This version of
map
accepts 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
map
could be called with?
and return the (first) failure. Since theEither
nonsense really pollutes the function, we could go one further. The unwrapping and forwarding logic could be moved into a helper:Helpers like
defer-errorsN
can 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).map
could 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
map
is, 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 likemap
andfilter
. 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
fail
to 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 inEither
case matches in the future.Beta Was this translation helpful? Give feedback.
All reactions