Pre-RFC: try patterns

Overview

add new syntax so that let Some(x?) = ...; becomes equivalent to let Some(x) = ...; let x = x?;.

the main usecase for this is when working with fallible iterators (like std::io::Lines) and similar constructs (anything that returns Result<Option<T>, E> or Option<Result<T, E>>).

Details

add a new unstable feature try_patterns to rustc. this would extend PatternWithoutRange with a new TryPattern case, defined as such:

Syntax
TryPattern :
PatternWithoutRange ?

this syntax may or may not be actually implemented via a desugaring pass.

Examples

Example 1

for ln? in std::io::stdin().lines() {
   println!("{}", ln);
}

desugared:

for ln in std::io::stdin().lines() {
   let ln = ln?;
   println!("{}", ln);
}

Example 2

the pattern does not have to be an identifier (although early iterations may have that limitation for ease of implementation)

if let Some(Value::Bool(true)?) = Some(serde_json::from_str("true")) {
  println!("it is true");
}

desugared:

let Some(_tmp0) = serde_json::from_str("true");
if let Value::Bool(true) = _tmp0? {
  println!("it is true");
}
3 Likes

I don't know what the perfect syntax is for this, but I very much would love to see a feature for this, and for await as well.

2 Likes

I think your example 2 is already achievable by Option::transpose, though it's a bit clearer IMO with the try pattern:

if let Some(Value::Bool(true)) = Some(serde_json::from_str("true")).transpose()? {

Though, I've written your example 1 a bunch of times and would love the try patterns to make that case simpler.

2 Likes

If we want to maintain that patterns are a dual of expressions, then pat? is the wrong syntax; it's probably try pat instead, such that matches!(try { 1 }, try 1).

I know there's some work open on readjusting how default binding modes function for edition2024. With that work, deref patterns, and the desire for try/await functionality in pattern position, we could choose to admit patterns are imperfect duals and extend the pattern syntax however is most convenient.

But while "rebinding modes" would certainly be convenient, I do still question the choice to make them an "infallible" pattern with a side effect. Modifying example 2 from the OP:

match serde_json::from_str(s) {
    try Value::Bool(true) => println!("it is true"),
    _ => println!("it isn't true"),
}

That this returns on errors and doesn't handle them via fallthrough to the next pattern is imho quite unclear. If it's limited like explicit binding modes are to being applied on name bindings only, it's a bit easier to digest, but still suffers the same issue.

Even await patterns, which aren't inherently fallible, immediately run into the same issue if they decorate a fallible pattern. Consider:

match stream.next() {
    await Value::Bool(true) => handle_true(),
    other => handle_other(other),
}

The first pattern already awaited the scrutinee here, so what does attempting to bind the scrutinee to other even do, rewind the future?

If we implement any "rebinding modes," they should be restricted at least to only decorating infallible patterns to avoid the obvious problem cases. A more general solution for fallible patterns that I used when discussing deref patterns' semantics is to extend if guards to match guards, meaning something like

match serde_json::from_str(s) {
    Some(_tmp0) match _tmp0? {
        Value::Bool(true) => println!("it is true"),
    },
    _ => println!("it isn't true"),
}

where the inner match doesn't need to be exhaustive, and any uncovered fallthrough of the inner match continues on to testing the outer match's following arms. The catch is that creating the inner scrutinee needs to work by-ref on the outer scrutinee if pattern matching is going to continue if the inner match fails, illustrating more directly the issue with "rebinding modes" that can fail.

3 Likes

Yeah, I agree; this is part of why I was saying I don't know the right syntax for this.

I don't think that's the right fit. try as a pattern seems more like a generalized version of Result or Option, but doesn't convey the concept that ? does of returning on errors.

1 Like

the main reason i prefer ? over try is for ease of scanning exit points for a function.

currently, there are two tokens that can cause an early return: return, and ? (and also any macro invocation). adding try to that list may complicate things. this would be even more confusing if you had a try block inside a const block inside a pattern, in which case a heuristic like "try in a pattern means exit if error" would no longer be accurate.

"normal" patterns only have two outcomes: match or no match. try patterns are an outlier, they can either match, not match, or return an error.

there's also already plenty of cases exceptions to "patterns are the dual of expression", such as identifier patterns (@ doesn't even mean anything in an expression context). notably, the one other way to possibly return from a pattern context, guard patterns, which take an arbitrary expression (possibly containing a return).

indeed, if they are approved, i think desugaring into to guard patterns would probably be the easiest way to implement this.

4 Likes

Simply desugaring to if guards is not a viable option, because if guards are only able to use the scrutinee by-ref, and ? is by-move. A successful match being able to inspect the scrutinee without moving from it is technically possible if the Try trait is changed to support it, but the desugaring needs to be able to move from the scrutinee in the break case.

Yeah, I agree. I'd be very hesitant to introduce any new construct that causes early exit from a function.

5 Likes

i'm inclined to agree, i think i'd rather not have this at all than have it with the "wrong syntax"