Pre-RFC: flexible `try fn`

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:

My proposed direction: async and try are both effects, and should work similarly

  • You never need to use async nor try nor await nor ?; you can use combinators instead, or call methods, or just match, or implement the traits directly
  • You “undo” async with await; you “undo” try with ?
  • Both have carrier types: For async/await it’s impl Async<Output = T>; for try/? today it’s impl Try<Ok = T> (though likely to change – see below – maybe impl Bubble<Output = T>)
  • There are both “delayable” effects: you can call foo() at one point – maybe not even in async/try context – then await/? it later
    • (In comparison, const and unsafe don’t have carrier types and can’t be delayed – you of course can’t do let x = foo(); let y = unsafe { x }; the way you can do let x = foo() let y = x?; or let x = foo(); let y = await!(x);)
  • Today we have .try_from() and TryFrom naming conventions where there’s conflicts with infallible; I wouldn’t be surprised to also get .read_async() and ReadAsync 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 a try {} block to write fallible code in “similar to infallible” style, with explicit markers in both cases for the things that are actually async/try
    • (Note that “what happens with return in a try block?” was a point of contention in the try RFC, and it’s just not allowed in an async block, so one simple and can-always-be-loosened-later option would be to prohibit it in try blocks too, initially.)
  • If a whole function or closure wants that style, then it can be defined as async fn or async ||, and, I would propose try fn or try ||
    • The async RFC decided that it’s async move ||, so it should also be try move ||
  • The body of such a function, block, or closure is automatically wrapped in the appropriate carrier type – it’s async { 4 } or try { 4 }, not async { Poll::Ready(4) } or try { Some(4) } – including a return 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 :slightly_smiling_face:)

(Oh man, that came out way longer than I was expecting…)

12 Likes