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
async
nortry
norawait
nor?
; you can use combinators instead, or call methods, or justmatch
, or implement the traits directly - You “undo”
async
withawait
; you “undo”try
with?
- Both have carrier types: For
async
/await
it’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
/try
context – thenawait
/?
it later- (In comparison,
const
andunsafe
don’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()
andTryFrom
naming conventions where there’s conflicts with infallible; I wouldn’t be surprised to also get.read_async()
andReadAsync
naming 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
return
in atry
block?” was a point of contention in thetry
RFC, and it’s just not allowed in anasync
block, so one simple and can-always-be-loosened-later option would be to prohibit it intry
blocks too, initially.)
- (Note that “what happens with
- If a whole function or closure wants that style, then it can be defined as
async fn
orasync ||
, and, I would proposetry fn
ortry ||
- The
async
RFC 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 areturn
expression 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…)