Skip to content
Permalink
Tree: 718aba9625
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
243 lines (191 sloc) 8.86 KB
  • Feature Name: refutable_let
  • Start Date: (fill me in with today's date, YYYY-MM-DD)
  • RFC PR: (leave this empty)
  • Rust Issue: (leave this empty)

Summary

This RFC is a continuation of the postponed RFC #1303. Where a regular let binding takes an irrefutable pattern and binds it to the containing scope, and if let takes a refutable binding and binds it to a new scope, let else takes a refutable binding and binds it to the containing scope. It does this by providing a block of code to run if the binding fails which is required to diverge, that is, to unconditionally break, continue, return, or call some diverging fn (-> !).

Motivation

The primary use case for refutable let is to reduce indentation on the happy path. Consider the case where you have an enum and a function that only handles one case:

enum Token {
    Ident(Symbol, Span),
    // other variants
}

fn do_something_with_token_ident(ident: Token) {
    // implementation
}

In current Rust, the obvious way to handle this is to use if let to destructure the Ident:

fn do_something_with_token_ident(ident: Token) {
    if let Token::Ident(symbol, span) = ident {
        // multiple lines
        // that use
        // symbol and span
    } else {
        panic!("do_something_with_token_ident only takes Token::Ident");
    }
}

This needlessly indents the happy path and separates the error handling from where the binding occurs. A slightly more clever Rustacean might use a match to mitigate this problem:

fn do_something_with_token_ident(ident: Token) {
    let (symbol, span) = match ident {
        Token::Ident(symbol, span) => (symbol, span),
        _ => panic!("do_something_with_token_ident only takes Token::Ident"),
    }
    // multiple lines
    // that use
    // symbol and span
}

This is better, because the error handling case is closer to the failable operation. However, this is still problematic, because it requires (symbol, span) to be written three times. Under this RFC, it would be possible to write:

fn do_something_with_token_ident(ident: Token) {
    let Token::Ident(symbol, span) = ident else {
        panic!("do_something_with_token_ident only takes Token::Ident");
    }
    // multiple lines
    // that use
    // symbol and span
}

This example would probably be better served by refactoring to allow do_something_with_token_ident to ensure it gets a Token::Ident at the type level, by introducing a TokenIdent struct which Token::Ident wraps, or some other method. However, in many cases this kind of refactoring would be prohibitively expensive or outright impossible.

Consider when you have a loop over some syn::Stmt and you want to do some process for just the assignment statements. This is done in MIRI using rustc types, but here we use syn for the purpose of using the more public API. With this RFC, one could get this behavior with the following code:

let syn::Stmt::Local(syn::Local {
        pat,
        ty,
        init,
        ..
    }) = stmt
else {
    continue;
}
// do something

The solution using if let would needlessly indent the happy path a level and separate the special-case handling from the binding. The match solution requires repeating (pat, ty, init) in this case, and the amount of repetition (read: silent ordering mismatch opportunities) grows as more complicated and/or deeply nested types are destructured.

Guide-level explanation

TODO: For now, see motivation.

Reference-level explanation

let else is a simple desugar similar to that of if let, to the match formulation that it replaces. The key difference is that the failed-binding branch is required to diverge by assigning it the ! type, rather than allowing that branch to produce the (implementation detail) tuple used to extract the bindings into the containing scope.

let PATTERN = EXPRESSION else {
    BLOCK
}

desugars to

let (B,I,N,D,I,N,G,S) = match EXPRESSION {
    PATTERN => (B,I,N,D,I,N,G,S),
    _ => {
        let _: ! {
            BLOCK
        }
    }
}

where (B,I,N,D,I,N,G,S) represents a tuple of all bound names in the pattern. This does not actually have to restrict itself to just bindings, but can also include unit-variant structs (which look like bindings until type information); this will require the compiler to unify the unit-variant again but does not affect the created bindings.

There is an edge-case ambiguity with this syntax. The problem is that let PATTERN = if EXPRESSION { BLOCK } is valid syntax today. That means that let PATTERN = if EXPRESSION { BLOCK } else { BLOCK } has two different valid parses under this RFC:

let PATTERN = ( if EXPRESSION { BLOCK } ) else { BLOCK }
let PATTERN = ( if EXPRESSION { BLOCK }   else { BLOCK } )

However, mitigating this issue is that if EXPRESSION { BLOCK } is only a valid rvalue when BLOCK's type is (). If the type is anything else, said fragment will fail to compile complaining about the missing else block.

In order to maintain backwards compatibility, the currently compiling code of let a = if b { c } else { d } must remain interpreted in the same manner. This RFC maintains that this is the correct resolution of this ambiguity, and that the other resolution in favor of let else would fail type check in all non-trivial cases, and would be meaningless when valid code.

To clarify,

let PATTERN = if EXPRESSION {
   BLOCK(THEN)
} else {
    BLOCK(ELSE)
}

should parse as it does today, and

let PATTERN = if EXPRESSION {
    BLOCK(THEN)
} else {
    BLOCK(ELSE)
} else {
    BLOCK(DIVERGE)
}

should parse as a let else where the expression is the if else expression and the diverging else is the second. Usage of complicated expressions in this position could be linted against (e.g. in clippy), but the RFC makes no case that restrictions beyond that of the expression in let/if let should be levied on the expression of a let else.

Drawbacks

TODO: see internals thread(s).

Rationale and alternatives

TODO: see internals thread(s).

Prior art

TODO.

  • Swift guard let
  • Others?

Unresolved questions

TODO: see internals thread(s).

You can’t perform that action at this time.