RFC 2175: if-while-or-patterns

lang (syntax | patterns | expressions)

Summary

Enables "or" patterns for if let and while let expressions as well as let and for statements. In other words, examples like the following are now possible:

enum E<T> {
    A(T), B(T), C, D, E, F
}

// Assume the enum E and the following for the remainder of the RFC:
use E::*;

let x = A(1);
let r = if let C | D = x { 1 } else { 2 };

while let A(x) | B(x) = source() {
    react_to(x);
}

enum ParameterKind<T, L = T> { Ty(T), Lifetime(L), }
use ParameterKind::*;

// Only possible when `L = T` such that `kind : ParameterKind<T, T>`.
let Ty(x) | Lifetime(x) = kind;

for Ty(x) | Lifetime(x) in ::std::iter::once(kind);

Motivation

While nothing in this RFC is currently impossible in Rust, the changes the RFC proposes improves the ergonomics of control flow when dealing with enums (sum types) with three or more variants where the program should react in one way to a group of variants, and another way to another group of variants. Examples of when such sum types occur are protocols, when dealing with languages (ASTs), and non-trivial iterators.

The following snippet (written with this RFC):

if let A(x) | B(x) = expr {
    do_stuff_with(x);
}

must be written as:

if let A(x) = expr {
    do_stuff_with(x);
} else if let B(x) = expr {
    do_stuff_with(x);
}

or, using match:

match expr {
    A(x) | B(x) => do_stuff_with(x),
    _           => {},
}

This way of using match is seen multiple times in std::iter when dealing with the Chain iterator adapter. An example of this is:

    fn fold<Acc, F>(self, init: Acc, mut f: F) -> Acc
        where F: FnMut(Acc, Self::Item) -> Acc,
    {
        let mut accum = init;
        match self.state {
            ChainState::Both | ChainState::Front => {
                accum = self.a.fold(accum, &mut f);
            }
            _ => { }
        }
        match self.state {
            ChainState::Both | ChainState::Back => {
                accum = self.b.fold(accum, &mut f);
            }
            _ => { }
        }
        accum
    }

which could have been written as:

    fn fold<Acc, F>(self, init: Acc, mut f: F) -> Acc
        where F: FnMut(Acc, Self::Item) -> Acc,
    {
        use ChainState::*;
        let mut accum = init;
        if let Both | Front = self.state { accum = self.a.fold(accum, &mut f); }
        if let Both | Back  = self.state { accum = self.b.fold(accum, &mut f); }
        accum
    }

This version is both shorter and clearer.

With while let, the ergonomics and in particular the readability can be significantly improved.

The following snippet (written with this RFC):

while let A(x) | B(x) = source() {
    react_to(x);
}

must currently be written as:

loop {
    match source() {
        A(x) | B(x) => react_to(x),
        _ => { break; }
    }
}

Another major motivation of the RFC is consistency with match.

To keep let and for statements consistent with if let, and to enable the scenario exemplified by ParameterKind in the motivation, these or-patterns are allowed at the top level of let and for statements.

In addition to the ParameterKind example, we can also consider slice.binary_search(&x). If we are only interested in the index at where x is or would be, without any regard for if it was there or not, we can now simply write:

let Ok(index) | Err(index) = slice.binary_search(&x);

and we will get back the index in any case and continue on from there.

Guide-level explanation

RFC 2005, in describing the third example in the section "Examples", refers to patterns with | in them as "or" patterns. This RFC adopts the same terminology.

While the "sum" of all patterns in match must be irrefutable, or in other words: cover all cases, be exhaustive, this is not the case (currently) with if/while let, which may have a refutable pattern. This RFC does not change this.

The RFC only extends the use of or-patterns at the top level from matches to if let and while let expressions as well as let and for statements.

For examples, see motivation.

Reference-level explanation

Grammar

if let

The grammar in § 7.2.24 is changed from:

if_let_expr : "if" "let" pat '=' expr '{' block '}'
               else_tail ? ;

to:

if_let_expr : "if" "let" '|'? pat [ '|' pat ] * '=' expr '{' block '}'
               else_tail ? ;

while let

The grammar in § 7.2.25 is changed from:

while_let_expr : [ lifetime ':' ] ? "while" "let" pat '=' expr '{' block '}' ;

to:

while_let_expr : [ lifetime ':' ] ? "while" "let" '|'? pat [ '|' pat ] * '=' expr '{' block '}' ;

for

The expr_for grammar is changed from:

expr_for : maybe_label FOR pat IN expr_nostruct block ;

to:

expr_for : maybe_label FOR '|'? pat ('|' pat)* IN expr_nostruct block ;

let statements

The statement stmt grammar is replaced with a language equivalent to:

stmt ::= old_stmt_grammar
       | let_stmt_many
       ;

let_stmt_many ::= "let" pat_two_plus "=" expr ";"

pat_two_plus ::= '|'? pat [ '|' pat ] + ;

Syntax lowering

The changes proposed in this RFC with respect to if let, while let, and for can be implemented by transforming the if/while let constructs with a syntax-lowering pass into match and loop + match expressions.

Meanwhile, let statements can be transformed into a continuation with match as described below.

Examples, if let

These examples are extensions on the if let RFC. Therefore, the RFC avoids duplicating any details already specified there.

Source:

if let |? PAT [| PAT]* = EXPR { BODY }

Result:

match EXPR {
    PAT [| PAT]* => { BODY }
    _ => {}
}

Source:

if let |? PAT [| PAT]* = EXPR { BODY_IF } else { BODY_ELSE }

Result:

match EXPR {
    PAT [| PAT]* => { BODY_IF }
    _ => { BODY_ELSE }
}

Source:

if COND {
    BODY_IF
} else if let |? PAT [| PAT]* = EXPR {
    BODY_ELSE_IF
} else {
    BODY_ELSE
}

Result:

if COND {
    BODY_IF
} else {
    match EXPR {
        |? PAT [| PAT]* => { BODY_ELSE_IF }
        _ => { BODY_ELSE }
    }
}

Source

if let |? PAT [| PAT]* = EXPR {
    BODY_IF
} else if COND {
    BODY_ELSE_IF_1
} else if OTHER_COND {
    BODY_ELSE_IF_2
}

Result:

match EXPR {
    |? PAT [| PAT]* => { BODY_IF }
    _ if COND => { BODY_ELSE_IF_1 }
    _ if OTHER_COND => { BODY_ELSE_IF_2 }
    _ => {}
}

Examples, while let

The following example is an extension on the while let RFC.

Source

['label:] while let |? PAT [| PAT]* = EXPR {
    BODY
}

Result:

['label:] loop {
    match EXPR {
        PAT [| PAT]* => BODY,
        _ => break
    }
}

Examples, for

Assuming that the semantics of for is defined by a desugaring from:

for PAT in EXPR_ITER {
    BODY
}

into:

match IntoIterator::into_iter(EXPR_ITER) {
    mut iter => loop {
        let next = match iter.next() {
            Some(val) => val,
            None => break,
        };
        let PAT = next;
        { BODY };
    },
};

then the only thing that changes is that PAT may include | at the top level in the for loop and the desugaring as per the section on grammar.

Desugaring let statements with | in the top-level pattern

There continues to be an exhaustivity check in let statements, however this check will now be able to support multiple patterns.

This is a possible desugaring that a Rust compiler may do. While such a compiler may elect to implement this differently, these semantics should be kept.

Source:

{
    // prefix of statements:
    stmt*
    // The let statement which is the cause for desugaring:
    let_stmt_many
    // the continuation / suffix of statements:
    stmt*
    tail_expr? // Meta-variable for optional tail expression without ; at end
}

Result

{
    stmt*
    match expr {
        pat_two_plus => {
            stmt*
            tail_expr?
        }
    }
}

For example, the following code:

{
    foo();
    bar();
    let Ok(index) | Err(index) = slice.binary_search(&thing);
    println!("{}", index);
    do_something_to(index)
}

can be desugared to

{
    foo();
    bar();
    match slice.binary_search(&thing) {
        Ok(index) | Err(index) => {
            println!("{}", index);
            do_something_to(index)
        }
    }
}

It can also be desugared to:

{
    foo();
    bar();
    let index = match slice.binary_search(&thing) {
        Ok(index) | Err(index) => index,
    }
    println!("{}", index);
    do_something_to(index)
}

(Both are equivalent)

Drawbacks

This adds more additions to the grammar and makes the compiler more complex.

Rationale and alternatives

This could simply not be done. Consistency with match is however on its own reason enough to do this.

It could be claimed that the if/while let RFCs already mandate this RFC, this RFC does answer that question and instead simply mandates it now.

Another alternative is to only deal with if/while let expressions but not let and for statements.

Unresolved questions

The exact syntax transformations should be deferred to the implementation. This RFC does not mandate exactly how the AST:s should be transformed, only that the or-pattern feature be supported.

There are no unresolved questions.