I don’t think this is “extreme” at all – I was planning on writing something very similar once the initial async fn and try {} implementations landed in the compiler.
More prior art:
- RFC #2107, Ok wrapping: Improved support for writing code from an error handling mindset
- Pre-RFC: Throwing Functions
My proposed direction: async and try are both effects, and should work similarly
- You never need to use
asyncnortrynorawaitnor?; you can use combinators instead, or call methods, or justmatch, or implement the traits directly - You “undo”
asyncwithawait; you “undo”trywith? - Both have carrier types: For
async/awaitit’simpl Async<Output = T>; fortry/?today it’simpl Try<Ok = T>(though likely to change – see below – maybeimpl Bubble<Output = T>) - There are both “delayable” effects: you can call
foo()at one point – maybe not even inasync/trycontext – thenawait/?it later- (In comparison,
constandunsafedon’t have carrier types and can’t be delayed – you of course can’t dolet x = foo(); let y = unsafe { x };the way you can dolet x = foo() let y = x?;orlet x = foo(); let y = await!(x);)
- (In comparison,
- Today we have
.try_from()andTryFromnaming conventions where there’s conflicts with infallible; I wouldn’t be surprised to also get.read_async()andReadAsyncnaming conventions where there’s conflicts with synchronous - Accepted RFCs have it so that you can use an
async {}block to write asynchronous code in “similar to synchronous” style, and so that you can use atry {}block to write fallible code in “similar to infallible” style, with explicit markers in both cases for the things that are actuallyasync/try- (Note that “what happens with
returnin atryblock?” was a point of contention in thetryRFC, and it’s just not allowed in anasyncblock, so one simple and can-always-be-loosened-later option would be to prohibit it intryblocks too, initially.)
- (Note that “what happens with
- If a whole function or closure wants that style, then it can be defined as
async fnorasync ||, and, I would proposetry fnortry ||- The
asyncRFC decided that it’sasync move ||, so it should also betry move ||
- The
- The body of such a function, block, or closure is automatically wrapped in the appropriate carrier type – it’s
async { 4 }ortry { 4 }, notasync { Poll::Ready(4) }ortry { Some(4) }– including areturnexpression in a function or closure
One difference is that await is only allowed in a context that’s explicitly been marked async, whereas ? can be used with just a compatible return type. We certainly shouldn’t remove that immediately, but I’d definitely be in favour of an opt-in clippy lint to block use of ? outside of try, because today ? is more than a fallibility marker, it’s also a “I’m thinking of the code here in the success-is-continue model”. And I think that an individual call is the wrong granularity for that choice: it should be a block (including, perhaps, the block that’s the body of a function). I personally find something like return Err(foo()?) dissonant, since the ? implies that continuing on is success, but then it’s not. (I also wonder if those opposed to ? would be less so if there was try as a “warning, you should expect ?s in here” marker around any use thereof.)
Similarly, I think it would make sense to start with a throw expression that’s allowed only in try context, since that immediately gets rid of the “when should I write throw x instead of return Err(x) question”. Whether it would stay that way I don’t know, though, since @Centril pointed out that it makes it harder to use in a broadly-applicable macro.
The other big question is how to write return types. RFC 2394 calls it a “complicated question”, ending up picking -> T instead of -> impl Async<Output=T> because of lifetime elision implications and a lack of need for polymorphic return. I’ve yet to see a -> T throws E syntax I’m happy with, so prefer just -> Result<T, E>. Interestingly, I think try hits exactly the opposite arguments as async here: a try fn doesn’t need to capture all the input lifetimes, so there aren’t the same gotchas with the full type, and since ?and try{} are polymorphic today, I think it’s important for a try fn to also support non-Result return types.
As for the traits, see the try_trait tracking issue. Specifically, this comment for your TryOk and this comment for your TryThrow. They’re roughly similar, though with some different choices around associated types and bounds to achieve goals like "typeof(x?) depends only on typeof(x), not on context". (And note the mention of “try functions” in that thread as well
)
(Oh man, that came out way longer than I was expecting…)