-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathExceptions.txt
632 lines (503 loc) · 29.3 KB
/
Exceptions.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
Language-integrated Result monad (a.k.a. checked exceptions)
Many people suggest general Haskell-style monadic `do` notation to make working with `Result`s nicer, but this is hard to integrate well with the rest of the language (control flow constructs). But approaching it from the opposite direction and adding `Result` semantics to the language natively (to the "ambient monad") to achieve the same goal, just as Rust already does with the ST and IO monads, is extremely straightforward. The result uses the syntax of checked exceptions, but inherits none of the mistakes from existing languages with that feature.
The meaning of the new constructs can be straightforwardly defined as a source-to-source translation to the existing language with `Result`s (see "implementation strategies" section), but need not necessarily be implemented as such.
NOTE
I've since become fond of an alternative "polymorphic synthesis" formulation which also reintroduces the `?` operator, see far below.
But don't skip, because that part builds on the stuff in these parts!
## Throwing:
fn foo(Bar) -> Baz throws bool { ... }
`throw false` has type `!`, argument type checked against `throws` clause
as with return type, so e.g. `fn foo() throws Box<Any> { throw box 666; }` works
every fn/expr has _single_ exception type
can throw any type
algebraically isomorphic to returning `Result`, but exception is propagated by default
functions without a `throws` clause never throw, i.e. the default is `throws !`
just like functions which never return are `-> !`
(! is uninhabited type which cannot be constructed, means "this can never happen")
basically this is making the Either (Result) monad a first-class part of the language, just like we've already done with ST and IO
this feels good! right track
contract violations are *not* part of `throws` clause and not catchable!!
assert/unwrap, array out-of-bounds, RefCell borrow check failure, ...
have to add `throws` to type of `fn` pointers, and additional typaram to `Fn` traits, e.g. `trait Fn<Args, Ret, Err>`
in surface syntax sugar, Ret defaults to `()` if `->` omitted, and Err to `!` if `throws` omitted
this seems reasonable and well-contained
other existing things mostly unaffected
## Catching:
try { EXPR } catch IRR-PAT { EXPR }
OR
try { EXPR } catch { PAT => EXPR, PAT => EXPR, ... }
#2 preferable if want to branch on exception, #1 if not
unavoidable excessive rightward drift in one case or the other with only one of these, so have both
try-catch is itself an expression! much like if-else
types of `try` and `catch` bodies must unify
throwable exceptions in `try` must unify to a type `E`
if no exception is thrown, try-catch evaluates to value of `try` block
if exception is thrown, it is passed to `catch` block, and try-catch evaluates to value of `catch` block
easy translation between exceptions and `Result`:
fn result<T, E, Body: FnOnce() -> T throws E>(body: Body) -> Result<T, E> {
try { Ok(body()) } catch err { Err(err) }
}
macro_rules! result { ... } // `result! { ... }` => `result(|| { ... })`
fn unwrap<T, E>(result: Result<T, E>) -> T throws E {
match result {
Ok(a) => a,
Err(e) => throw e
}
}
`result` and `unwrap` witness the isomorphism between `-> A throws B` and `-> Result<A, B>`
## Impact
for full generality HOFs should change to e.g.
fn map<A, B, E>(vec: Vec<A>, f: |A| -> B throws E) -> Vec<B> throws E;
actually, does this make sense here?
if `f` throws an exception both old and new vecs will be lost...
but same thing def should make sense for other HOFs (and e.g. `map` taking `&Vec` instead of by move)
currently you *can't* short-circuit out of map in any way!
so this is a Pareto-improvement!
polymorphism over exception types is additional expressiveness relative to current language
but even w/ current `map()` as-is, can use it with throwing functions by converting to `Result`s with `result!` (and afterwards `unwrap` to rethrow if desired)
notably the body of `map` doesn't need to change in any way, only its type(?)
but *don't* need to add E params everywhere: if something doesn't currently return a Result<T, E>, there's no reason for it to throw an E, either!!!
`throws` is analogous to `Result`, NOT to `fail!()`
it is completely reasonable for most functions not to throw anything!!
(do not fall into Java trap of exceptions = shiny toy, want to use them everywhere)
basically an `fn` is a good candidate for `throws` only if either:
* it currently returns `Result`, or
* it's a HOF and might want to be generic over the closure's exception type (an expressiveness gain)
## Motivation
You should almost never use exceptions where you don't currently use `Result`, the exception is HOFs where you may want to parameterize over the exception type, improving expressiveness.
It's painfully clear that e.g. we would want to use `throws IoError` in `std::io`!
Any lib which defines `type MyResult<T> = Result<T, MyError>` and uses it everywhere would want to use `throws`.
There are a few of these!
(Maybe it's even a best practice for each lib to have its own error enum like this. Maybe the lack of union types (multiple throwable exception types) is not a bad thing at all!! Encourages good practice of encapsulating errors from upstream.)
(future: interacts nicely w/ datasort refinements?)
If an interface doesn't allow throwing exceptions, but you want to call exception-throwing methods in impl (also for e.g. HOF args), you can convert to a Result with `result!`. Any impedance mismatch is dead easy to resolve.
This is not a world-changing feature, just an incremental improvement over working with `Result`s.
Might seem like a major feature, but really... not that much.
Really *just* a control flow construct!! No hand-waving about "truly exceptional circumstances" & similar bullshit!! Choice between `Result` and `throws` based on ergonomics _only_.
We're already using checked exceptions with Result<T, E>, we're just doing the propagation by hand, and `T throws E` is just a `Result<T, E>` where propagation happens in the language.
Benefits of `throws`:
- Automatically propagated
- Can handle exceptions from multiple function calls in a single block without cluttering control flow
Use `throws` if:
- Callers may want to call many functions which throw the same exception type
- Callers want to propagate exceptions
misc tangential note: handling fail!() at task boundaries is a form of harm reduction. I.e. the program has a flaw (or perhaps environment, configuration does), but maybe we can fail less-than-catastrophically. Aborting program and writing "fix your program, dumbass" to console not so awesome in production environment.
## Problems with checked exceptions in Java:
- All exceptions must derive from an `Exception` class. Cannot throw any type.
- Bad/incomplete support for being generic over exception types:
http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/
http://c2.com/cgi/wiki?TheProblemWithCheckedExceptions
related to throwing multiple, rather than single exception type
- Severe over(ab)use in common APIs
- APIs have exception types which don't contain any useful information, just an error string => no reasonable way to handle them besides ignoring/logging/aborting
- They don't have a `Result` type to "bottle up" an exception to bridge signature mismatches (e.g. with J8 lambdas).
Rust doesn't have, nor needs to copy, _any_ of these mistakes!!
http://twistedoakstudios.com/blog/Post399_improving-checked-exceptions
Verbosity of declaring exception types: not a problem in Rust. Declare an enum of possible errors, throw that enum.
Abstracting over exception types also not a problem in Rust. Rust generics are up to the task! (Single exception type rather than multiple doesn't hurt, either.)
Catching and rethrowing as different exception type might still be annoying. But: not any more so than the same thing with `Result`.
TODO add some examples, translate existing `Result` code
## QUESTIONS
### Impact on unsafe code?
Could even disallow throwing inside `unsafe`, require the exception type of `unsafe { }` to be `!`.
(Or just have a lint...)
Along similar lines... we could have task failure unwinding through an `unsafe { }` become a process abort.
This doesn't absolve `unsafe` from having to think about exception (failure, panic, despair) safety, but does prevent it from implicating type/memory safety/security. (=> harm reduction)
Would kinda imply that `unsafe fn` may not fail... is that bad?
### Destructors
Destructors (Drop::drop()), as the signature says, can't throw anything. Which is exactly as it should be.
### Do we need task-like isolation with Send here? What does exception safety mean?
"If you catch unwinding in a local heap and attempt resumption, you have no idea what state it's in."
"You still have to write in "worry about exceptions everywhere style inside a try-block or function-that-rethrows. Only get to avoid it when you're insulating yourself by the implicit "throw ()" declaration."
"What's problematic about exceptions in C++, and what forces you to "worry about exceptions everywhere", is that code inside a try-block and code outside of it may share state, which the code in the try block may be in various stages of modifying (along with allocating resources) when the exception is thrown, potentially leaving an inconsistent state and/or leaked resources at the point where the exception is caught. Therefore all functions have to be very careful that any mutable state accessible to the function is in (or reverts to) a consistent state at any point where an exception might be thrown, and that resources aren't left dangling, because who knows whether that mutable state might not be on the outside of a try-block and the function on the inside of it."
"The important conclusion to drive home from this discussion is that, to make a sequence of function calls strong, you need to structure it as a pure subsequence (possibly altering local automatic data), followed by no more than one strong call, followed by a nothrow subsequence. This is exactly what the “assignment-through-swap idiom” [6] does (actually, Dave proved that all strong functions have this form, but the proof does not fit in the margin of this text)." [note: by recursively "inlining" the one strong call in the middle, we can see that this actually boils down to just a pure subsequence then a nothrow subsequence]
http://www.reddit.com/r/rust/comments/2ejxk6/minutes_from_last_weeks_workweek_lots_here/ck4vekz
http://permalink.gmane.org/gmane.comp.lang.rust.devel/4051
http://www.jot.fm/issues/issue_2011_01/article1.pdf
http://erdani.com/publications/cuj-2003-12.pdf
Should `try` blocks have a `Send` requirement?
Is this necessary and/or sufficient?
`Send` seems like both too much and not enough:
`&` refs are not `Send` but should be completely OK, except for maybe `&Cell` & co.
But if `&Cell` is a problem, then so is `Arc<AtomicFoo>`, which /is/ `Send`
So what exactly do we want?
Can we do better than "best effort"?
Does this tie in to purity in any way... ?
Yes: if the `try` block is not passed any cap, then `&Cell` and `Arc<AtomicFoo>` are ruled out.
But then: what is the conclusion? Requiring `try` be pure would be ridiculous.
And we want to allow moving into it, I think.
The shape of things seems to be:
* We get the "basic" guarantee for free because of safety & destructors.
* We need some kind of restriction on `try` and/or throwing functions to get the strong guarantee.
Requirement for a strong fn seems to be (according to both papers): all throwing precedes all observable mutating.
Observable by whom?? apparently: function with `try` block
Is this restriction on functions more liberal or more restrictive than putting a restriction on `try`??
=> Think either of these on its own is a sufficient condition for strong guarantee!
Which restriction is less unpleasant?
Can the restriction on functions be done in the borrow checker?
How important is the strong guarantee? Is it worth a restriction?
W.r.t. Arc<AtomicFoo>:
We can't provide the strong guarantee for external IO, only for in-program structures. (neither can anyone else)
Atomic ops are basically IO. Same for Cell?
Given that safety is not implicated, how valuable is this incomplete strong guarantee?
Even in C++, usual recommendation is to provide strong guarantee only if cost is acceptably low, not otherwise.
If we don't enforce strong guarantee, then:
* The basic guarantee still comes for free
* Code which has to think about exception safety in the sense of the strong guarantee is:
- Code inside functions with a `throws` clause
- Code inside `try` blocks
but this should be something like 10% of all code at most.
So this is _much, much_ less bad than C++!
In C++:
100% of code has to think about exception safety,
they're not present in the types,
and even the basic guarantee has to be ensured manually.
Maybe if the "exception safety problem" is reduced by two orders of magnitude, then it stops being an "important problem", and is just a thing.
So the tradeoff is between 10% of code having to think about basic vs. strong guarantee, or a `Send`-like restriction on `try` blocks to enforce strong guarantee.
Experience would probably reveal which is preferable.
Or perhaps a compromise: provide restriction on `try` and/or throwing functions as a lint, let individual codebases enforce the strong guarantee if they want to.
### Do we want/need a finally clause?
nope, don't think so. only wanted for cleanup / resource release. have destructors. destructors can't throw. life is good.
### Interaction with linear types (&out)
exception is part of type, so we can easily forbid calling `throws` functions while an `&out` is in scope, just as we would forbid `return`
### Can+should we give any kind of meaning to a `try { ... }` block without a `catch`?
enforce that no exceptions are thrown? => doesn't seem useful
turn any exceptions into task failures? => doesn't seem wise, should be explicit
silently ignore any exceptions? => same here
return a `Result<T, E>`, i.e. instead of `result!`? => seems ad hoc and not that valuable
=> so maybe we could, but doesn't seem like we should
### Rethrowing as different exception, convenience thereof
Haskell:
withExcept :: (e -> e') -> Except e a -> Except e' a
withExcept f foo
withExcept (\e -> f e) foo
Rust:
try { foo() } catch e { throw f(e) }
fn with_except<T, E1, E2>(f: FnOnce(E1) -> E2, exp: FnOnce() -> T throws E1) -> T throws E2 {
try { exp() } catch e { throw f(e) }
}
with_except(|e| f(e), || foo()) // how much better is this?
with_except(f, || foo()) // a bit better.. but only for single-funcall
macro_rules! with_except { ... }
with_except!(|e| f(e), foo())
working around our awkward function call and lambda syntax here... `do` sugar?
do with_except(|e| f(e)) { foo() }
meh
`throw as` sugar? how exactly?
EXPR throw as ...?
specify a function? call out to a trait?
EXPR throw PAT as EXPR? is this syntactically unambiguous?
try { foo() } catch e { throw f(e) }
with_except(|e| f(e), || foo())
foo() throw e as f(e)
throw e as f(e) in foo()
throw e in foo() as f(e)
in foo() throw e as f(e)
try { foo() } throw as f(_) (partial application / implicit closure)
not sure how important this is... there's basically no language ever where this is convenient
and Rust is better than most
and it's probably still better than `match`ing on `Result`s
can maybe get close to the ideal w/ union types:
try {
throws_a();
throws_b();
throws_c();
} catch {
e is A => throw WrapA(e),
e is B => throw WrapB(e),
e is C => throw WrapC(e)
}
(still a bit of repetition in there.. a macro? some trait impled for A, B, and C?)
but the logic isn't usually this "dumb"
downstream exceptions /shouldn't/ usually map 1-to-1 with upstream! is bad design
### Misc
also from Haskell: `Alternative`: combine exceptions if type is Monoid/Semigroup?
`throw` would also interact pleasantly with `let`..`else`, which we should add
### throwing ()
Should we have an attitude about `fn foo() -> Foo throws () { ... }`?
It seems strange coming from traditional exception handling systems, and first reaction might be to say "don't do that". But this is *not* a traditional exception handling system!
It's completely isomorphic to returning `Option<Foo>` or `Result<Foo, ()>`, so it's possible that this should be /encouraged/, just as those are/were.
Do whatever works the best in practice. No preconceptions!
Allow just `throw` desugaring to `throw ()`, as with `return`?
### Implementation strategies
1. compile down to Result-alike
struct InternalResult<T, E> { InternalOk(T), InternalErr(E) }
fn foo() -> T throws E { ...CODE... }
becomes
fn foo() -> InternalResult<T, E> { InternalOk({ ...CODE... }) }
return x
becomes
return InternalOk(x)
throw x
becomes
return InternalErr(x)
bar(foo())
becomes
bar(match foo() { InternalOk(x) => x, InternalErr(y) => throw y })
try { bar(foo()) } catch e { baz(e) }
becomes
match (|| bar(foo()))() { InternalOk(x) => x, InternalErr(e) => baz(e) }
or actually: this would/should use not-yet-implemented "early return from any block" feature, not lambda!
(`return` inside `try` should return from whole function, not just `try` block!)
(see "polymorphic synthesis" alternative below)
...etc...
should be pretty mechanical
might want to add support for `!` as first class type to avoid having to special-case throwing vs. non-throwing functions
(but we should want to do that anyways)
2. unwinding
like C++ & current fail!()
probs easier than C++ because everything only throws single exception type, each `try` has only one `catch`, catches everything
(well, except for task failure...)
complication:
fn foo() throws Box<int> { ... }
fn bar() throws Box<Show> { foo() }
probably need some special handling here, e.g. implicit catch-and-rethrow around body of function
## ALTERNATIVES
### just current language + `?`
could just add postfix `?` operator to current language to propagate exceptions explicitly, as in aturon's proposal
on its own this is not too bad for writing, and maybe even a benefit for reading (throwing calls are syntactically distinguished)
for the record, could also require an `?` on throwing calls in the above proposal as-is, w/ no other changes!
so this should *not* be counted as a benefit of this alternative; at best, as the absence of a drawback!
but this is not the only thing: ergonomic costs across the board:
foo().bar().baz()
vs.
foo()?.bar()?.baz()
throw foo
vs.
return Err(foo)
return foo
vs.
return Ok(foo)
fn { ...; result }
vs.
fn { ...; Ok(result) }
try { foo().bar() } catch e { baz(e) }
vs.
match foo() { Ok(res) => res.bar(), Err(e) => baz(e) }
this is in the simplest case where only `foo()` throws!
gets progressively worse (pattern matching gunk) as the body of `try { }` does more things
basically:
* not only "throwing" but also "normal" code paths are penalized, and
* lack of try-catch is the biggest single pain point
main benefit of this approach is it avoids duplication
no need to choose between `throws` and `Result`, there is only `Result`
### a polymorphic synthesis?
...could maybe do a thing where the two are combined:
add `?` *and* `try`..`catch`,`throw` and `throws`
main idea is that _all_ of these "desugar" to being polymorphic over any appropriate type (Result, Option, etc.), using a trait
normal fn calls return the Result (or whichever type) directly, propagation must be explicitly requested with `?`
`?` inside a `try`-`catch` only propagates up to the `try`, not out of whole fn!
`return` still returns from whole fn
`throws` makes the fn polymorphic over any result-like-type, but can also use `?`, `throw` and `try`..`catch` with concrete result-like-types
just like for traits/type classes in general (can parameterize fn over `Eq`, but can also `==` concrete `int`s)
#### variations on the trait
all of these are equivalent!
meaning is essentially "is isomorphic to Result<Normal, Exception> for some types Normal and Exception"
#lang(carrier)
trait Carrier {
type Normal;
type Exception;
$METHODS
}
where $METHODS in:
1. explicit isomorphism with `Result`
fn from_result(Result<Normal, Exception>) -> Self;
fn to_result(Self) -> Result<Normal, Exception>;
laws:
from_result(to_result(x)) = x
to_result(from_result(x)) = x
laws for the others below should be "the same"!
left as an excercise for the reader
2. avoid mentioning `Result`, most naive version
fn normal(Normal) -> Self;
fn exception(Exception) -> Self;
fn is_normal(&Self) -> bool;
fn is_exception(&Self) -> bool;
fn assert_normal(Self) -> Normal;
fn assert_exception(Self) -> Exception;
3. destructuring w/ Scott
fn normal(Normal) -> Self;
fn exception(Exception) -> Self;
fn match_carrier<T>(Self, FnOnce(Normal) -> T, FnOnce(Exception) -> T) -> T;
probably the right approach for Haskell, & probably not for Rust
e.g. two closures cannot share environment! awkward.
4. such elegant, wow
fn normal(Normal) -> Self;
fn exception(Exception) -> Self;
fn translate<Other: Carrier<Normal=Normal, Exception=Exception>>(Self) -> Other;
can instantiate `Other` with any concrete carrier type, e.g. `Result` or `Option` (then e.g. pattern match on it, or w/e)
(`translate` smells like a natural transformation...)
5. Scott II
trait BiOnceFn {
type Args1;
type Args2;
type Ret;
fn call1(Self, Args1) -> Ret;
fn call2(Self, Args2) -> Ret;
}
trait Carrier {
type Normal;
type Exception;
fn normal(Normal) -> Self;
fn exception(Exception) -> Self;
fn match_carrier<T>(Self, BiOnceFn<Args1=Normal, Args2=Exception, Ret=T>) -> T;
}
now the cannot-share-environment problem is solved!
is generalization of 4. (with `call1()`=`normal()`, `call2()`=`exception()`)
...and there's probably more...
(e.g. a concrete `try_catch` method?)
#### `impl`s of `Carrier`
method defns for 1..3 should be obvious; 5. is structurally similar to 4.; will go with 4. to illustrate
impl<T, E> Carrier for Result<T, E> {
type Normal = T;
type Exception = E;
fn normal(a: T) -> Result<T, E> { Ok(a) }
fn exception(e: E) -> Result<T, E> { Err(e) }
fn translate<Other: Carrier<Normal=T, Exception=E>>(result: Result<T, E>) -> Other {
match result {
Ok(a) => normal(a),
Err(e) => exception(e)
}
}
}
impl<T> Carrier for Option<T> {
type Normal = T;
type Exception = ();
fn normal(a: T) -> Option<T> { Some(a) }
fn exception(e: ()) -> Option<T> { None }
fn translate<Other: Carrier<Normal=T, Exception=()>>(option: Option<T>) -> Other {
match option {
Some(a) => normal(a),
None => exception(())
}
}
}
impl Carrier for bool {
type Normal = ();
type Exception = ();
fn normal(a: ()) -> bool { true }
fn exception(e: ()) -> bool { false }
fn translate<Other: Carrier<Normal=(), Exception=()>>(b: bool) -> Other {
match b {
true => normal(()),
false => exception(())
}
}
}
should avoid getting too creative w/ evil impls!
e.g. impl for `Vec` with `Exception=()` and exception being the empty `Vec` would be quite icky
want to avoid this for the same reason that we don't consider empty `Vec` and `0` to be "false" in `if` condition
presumably an impl is OK iff: it obeys the laws! (so impl for `bool` is at worst too cute; but not evil)
translating between carrier types should be a no-op most of the time
we should have it so that
repr(bool) = repr(Option<()>) = repr(Result<(), ()>)
repr(Option<T>) = repr(Result<T, ()>)
etc.
#### Elaboration of constructs (finally)
involves "early return from any block" feature, which /should/ be added independently (but could also be done under the hood without exposing it)
existing `return EXPR` is extended with an optional lifetime/scope argument: `return 'a EXPR`
alternately could extend `break` with an optional value argument, & make it work for not just loops; the choice is irrelevant here
causes early exit from block 'a with the value EXPR (types of course must match)
default if not specified is to return from whole fn, like currently
iow magical lifetime 'fn which refers to outermost block of function
throw EXPR
=>
return 'here Carrier::exception(EXPR)
where 'here is innermost enclosing `try` block, or 'fn if none
EXPR?
=>
match translate(EXPR) { Ok(a) => a, Err(e) => throw e }
try { foo()?.bar() }
=>
'here: { Carrier::normal(foo()?.bar()) }
here it does make sense to define `try { }` without a catch as returning the carrier directly!
because we are defining everything in terms of them anyhow
and it's useful (coalesce multiple potential `?` propagations into single result)
try { foo()?.bar() } catch e { baz(e) }
=>
match try { foo()?.bar() } { Ok(a) => a, Err(e) => baz(e) }
try { foo()?.bar() } catch { A(e) => baz(e), B(e) => quux(e) }
=>
try { foo()?.bar() } catch e { match e { A(e) => baz(e), B(e) => quux(e) } }
(same thing, just more convenient to match immediately sometimes, & match rhymes with catch)
fn foo(A) -> B throws C { CODE }
=>
fn foo<Car: Carrier<Normal=B, Exception=C>>(A) -> Car { try { CODE } }
this accomplishes two things:
`foo` is polymorphic over carrier type
gets rid of syntactic overhead for "normal" case, e.g. CODE can be just `make_b()` instead of `Ok(make_b())`
extending `throws` to HOFs might be hairier, but seems like it should also work out OK, *if* we want to; just introduce Carrier typarams as necessary
(would also be simpler than OP, b/c `Fn`{,`Mut`,`Once`} would not need to be changed! could just desugar to the result type being a carrier)
typing rules for new constructs should be the same as for the elaborated expressions, or mostly anyways
if carrier type is ever underconstrained, could just default it to `Result` (would involve making it a lang item)
if we have explicit `?` to propagate we probably don't need to worry so much about exception safety either
(believe "exception unsafety" phenomenon partly arises due to propagation being invisible)
above I've defined each construct in terms of the previous ones to avoid wearing out my typing fingers
but they are relatively easily separable (just inline the removed ones)
could have just `?`, or `?` and `try`..`catch` but not `throw`/`throws`, etc.
but they're all nice & useful & well behaved
`throw` and `throws` are the weakest links
I love how cleanly they all fit together
## POTENTIAL FUTURE WORK
Union types
Would allow throwing/propagating multiple types of exceptions and catching based on type
I.e. like Java
Not clear if this is actually a good idea! Maybe it's always / almost always preferable to use enums and trait objects.
And there are some awkward questions and potentially far-reaching implications.
Union types:
Either<A, B, C, D> (variadic)
Can be any of A, B, C, or D
Like `Any`, but restricted to a set of listed types
Represented as (TypeId, UnsafeUnion<A, B, C, D>) (~ enums)
Either<Types..., T> = Either<Types...> if contains(Types, T)
also reordering. basically, a type-level set.
T is *implicitly* coerced to Either<..., T, ...>
can test contained type like with `Any`, or use type-match
QUESTION what if Types contains trait objects?
avoid trait object auto-coercing in this case?
QUESTION what if Types contains a type variable?
does this work out OK? forbid it?
QUESTION what if Types contains another `Either`?
would naively lead to nested TypeIds
do that? flatten it? forbid it?
should be able to go from A to Either<A, B, C> *and* from Either<A, B> to Either<A, B, C>...
QUESTION is this related to OCaml's polymorphic variants?
special syntax: (A|B|C), A || B || C, A or B or C, ... ?
(A|B|C) with `foo is Type` for matching works out OK (no conflict with disjunction patterns)
(A|B|C) is a closed trait! (see also privacy reform rfc)
Type match:
foo: (int|bool|char)
match foo {
i is int => ...
b is bool => ...
c is char => ...
}
or:
`i: int =>` (conflict: type ascription)
`i as int =>` (conflict: planned name binding)
`(type int, i) =>` (accurate but ugly)
because set of types is fixed, can check exhaustiveness
(perhaps also allow this for "unrestricted" Any and require a wildcard?)
QUESTION
if I write try { throws_a(); throws_b(); } catch ...
is the exception type *inferred* to be (A|B)?
if no => need to annotate every throwable type?
if yes => are they inferred in other places? why or why not?
e.g. `if foo { a } else { b }` shouldn't be inferred as (A|B)... that's ridiculous
...maybe it can be inferred from the catch block?
e.g. try { a(); b(); } catch { e is Foo => ..., e is Bar => ... }
we could(?) infer from the catch-block that the exception-type of the try-block is a union-type
## The two together:
fn some_op(arg: int) -> String throws (IoError|ApiError) { ... }
fn other_op(arg: int) throws IoError {
try {
println!("success: {}", some_op(arg))
} catch {
ae is ApiError => println!("api err: {}", ae),
ie is IoError => throw ie
}
// some_op(arg); error: can't throw ApiError
// throw 9i; error: can't throw int
}
have to list throwable type(s) explicitly, type error if you try to throw something not listed, but propagation is implicit
(unhandled errors must be rethrown explicitly... not sure if this is a problem)