rfcs/text/2497-if-let-chains.md

69 KiB
Raw Permalink Blame History

Summary

Extends if let and while let-expressions with chaining, allowing you to combine multiple lets and bool-typed conditions together naturally. After implementing this RFC, you'll be able to write, among other things:

fn param_env<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>, def_id: DefId) -> ParamEnv<'tcx> {
    if let Some(Def::Existential(_)) = tcx.describe_def(def_id)
        && let Some(node_id) = tcx.hir.as_local_node_id(def_id)
        && let hir::map::NodeItem(item) = tcx.hir.get(node_id)
        && let hir::ItemExistential(ref exist_ty) = item.node
        && let Some(parent) = exist_ty.impl_trait_fn
    {
        return param_env(tcx, parent);
    }

    ...
}

and with side effects:

while let Ok(user) = read_user(::std::io::stdin())
    && user.name == "Alan Turing"
    && let Ok(hobby) = read_hobby_of(&user)
{
    if hobby == "Hacking Enigma" {
        println!("Yep, It's you.");
        return Some(read_encrypted_stuff());
    } else {
        println!("You can't be Alan! ");
    }
}

return None;

The main aim of this RFC is to decide that this is a problem worth solving as well as discussing a few available options. Most importantly, we want to make if let PAT = EXPR && .. a possible option for Rust 2018.

Motivation

The main motivation for this RFC is improving readability, ergonomics, and reducing paper cuts.

Right-ward drift

Today, each if let needs a brace, which means that you usually, to keep the code readable, indent once to the right each time. Thus, matching multiple things quickly leads to way too much indent that overflows the typical text editor or IDE horizontally. This is in particular bad for readers that can only fit around 80-100 characters per line in their editor. Keeping in mind that code is read more than written, it is important to improve readability where possible.

Other solution: Tuples

One solution is matching a tuple, but that is a poor solution when there are side effects or expensive computations involved, and doesn't necessarily work as DSTs and lvalues can't go in tuples.

Other solution: break ...

Another solution to avoid right-ward drift is to create a new function for part of the indentation. When the inner scopes depend on a lot of variables and state from outer scopes, all of these variables have to be passed on to the newly created function, which may not even be a natural unit to abstract into a function. Creating a new function, especially one that feels artificial, can also inhibit local reasoning. A new level of function (or IIFE) also changes the behaviour of return, break, ?, and friends.

A third solution involves using the expression form break '<label>. You may then rewrite the snippet from the summary as:

fn param_env<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>, def_id: DefId) -> ParamEnv<'tcx> {
    'stop: {
        if let Some(Def::Existential(_)) = tcx.describe_def(def_id) {
        } else {
            break 'stop;
        };

        let node_id = if let Some(node_id) = tcx.hir.as_local_node_id(def_id) {
            node_id
        } else {
            break 'stop;
        }

        let item = if let hir::map::NodeItem(item) = tcx.hir.get(node_id) {
            item
        } else {
            break 'stop;
        };

        let exists_ty = if let hir::ItemExistential(ref exist_ty) = item.node {
            exists_ty
        } else {
            break 'stop;
        }

        if let Some(parent) = exist_ty.impl_trait_fn {
            return param_env(tcx, parent);
        }
    }

    ...
}

while right-ward drift has been reduced, a significant amount of line noise has been introduced. The user is also forced to track the label 'stop. All in all, this alternative significantly reduces readability wherefore we discourage from this way of writing.

Boiler-plate reduction using macros

One way to reduce the noise from the above alternative solution is to refactor some commonalities into a macro. However, refactoring into a macro means that you need to understand the macro. In comparison, chained if lets constitute something simpler that all Rust programmers will understand, as opposed to a specialized macro.

Mixing conditions and pattern matching

A match expression can have if guards, but if let currently requires another level of conditionals. This is particularly troublesome for cases that can't be matched, like x.fract() == 0, or error enums that disallow matching, like std::io::ErrorKind.

Duplicating code in else clauses

In some cases, you may have written something like:

if let A(x) = foo() {
    if let B(y) = bar(x) {
        do_stuff_with(x, y)
    } else {
        some_long_expression
    }
} else {
    some_long_expression
}

In this example foo() and bar(x) have side effects, but more crucially, there is a dependency between matching on the result of foo() to execute bar(x). Therefore, matching on (foo(), bar(x)) is not possible in this case because there's no x in scope. So you have no choice but to write it in this way (or use break 'label..).

However, now some_long_expression is repeated, and if more let bindings are added, more repetition ensues. To avoid repeating the long expression, you might encapsulate this in a new function, but that new function may feel like an artificial abstraction as discussed above.

This is problematic even with a macro to simplify, as it results in more code emitted that LLVM commonly cannot simplify.

Bringing the language closer to the mental model

The readability of programs is often about the degree to which the code corresponds to the mental model the reader has of said program. Therefore, we should aim to bring the language closer to the mental model of the reader. With respect to if let-expressions, rather than saying (out loud):

if A matches, and

if x holds and

if B matches

do X, Y, and Z

..it is more common to say:

If A matches, x holds, and B matches, do X, Y, and Z

This RFC is more in line with the latter formulation and thus brings the language closer to the readers mental model.

Instead of macros

As we've previously touched upon, we may define and use a macro to reduce boilerplate. A macro like if_chain! as a solution however has the problem of not being part of the language specification. Thus, it is not part of the common syntax that experienced Rust programmers are familiar with and is instead local to the project itself. The non-universality of syntax therefore hurts readability.

Plenty of Real-world use cases

We have already seen a real world example from the compiler in the summary. By taking a look at the reverse dependencies of if_chain! we can find more real-world use cases that this RFC facilitates.

As an example, clippy defines a function:

/// Returns the slice of format string parts in an `Arguments::new_v1` call.
fn get_argument_fmtstr_parts(expr: &Expr) -> Option<(InternedString, usize)> {
    if_chain! {
        if let ExprAddrOf(_, ref expr) = expr.node; // &["…", "…", …]
        if let ExprArray(ref exprs) = expr.node;
        if let Some(expr) = exprs.last();
        if let ExprLit(ref lit) = expr.node;
        if let LitKind::Str(ref lit, _) = lit.node;
        then {
            return Some((lit.as_str(), exprs.len()));
        }
    }
    None
}

with this RFC, this would be written, without any external dependencies, as:

/// Returns the slice of format string parts in an `Arguments::new_v1` call.
fn get_argument_fmtstr_parts(expr: &Expr) -> Option<(InternedString, usize)> {
    if let ExprAddrOf(_, ref expr) = expr.node // &["…", "…", …]
        && let ExprArray(ref exprs) = expr.node
        && let Some(expr) = exprs.last()
        && let ExprLit(ref lit) = expr.node
        && let LitKind::Str(ref lit, _) = lit.node
    {
        Some((lit.as_str(), exprs.len()))
    } else {
        None
    }
}

This kind of deep pattern matching is common for parsers and when dealing with ASTs. One place which deals with ASTs is the compiler itself as seen above. Thus, with this RFC, some compiler internals may be simplified. Another common place is when authoring with custom derive macros using the syn crate.

An expected feature

As demonstrated in Appendix B, the syntax proposed in this RFC is already expected to be allowed in Rust by users today. Indeed, the author of this RFC made this assumption at some point.

Unification

In today's Rust, there is both a grammatical and conceptual distinction between if and if let as well as while and while let. This RFC aims to erase the divide and unify concepts. Henceforth, there is just if and while. Thus if let is no longer the unit.

"Why now?"

A legitimate question to ask is:

Why implement this now?

In this case, the answer is simple: We can't wait.

Because Rust takes stability seriously, we would like to avoid any breakage in-between editions even if the breakage is exceedingly (as in the case of this RFC) unlikely. Instead, we want to deal with the vanishingly tiny degree of breakage, as explained in the reference-level-explanation, introduced by this RFC with the edition mechanism.

As it happens, a new edition "Rust 2018" is in the works at the moment (as of 2018-07-12). This is an excellent opportunity to take advantage of, and that is precisely what we aim to do here.

Guide-level explanation

This section examines the features proposed by this RFC.

if let-chains

An if let chain, refers to a chain of multiple let bindings, which may mixed with conditionals in an if expression.

An example of such a chain is:

if let A(x) = foo()
    && let B(y) = bar()
{
    computation_with(x, y)
}

It is important to note that this is not generally equivalent to the following expression:

if let (A(x), B(y)) = (foo(), bar()) {
    computation_with(x, y)
}

Unlike the first example, there is no short circuiting logic in the example using tuples. Assuming that there are no panics, which is usually the case, both functions are always executed in the latter example.

If we desugar the first example, we can clearly see the difference:

if let A(x) = foo() {
    if let B(y) = bar() {
        computation_with(x, y)
    }
}

What is the practical difference, and why is short circuiting behaviour an important distinction? The call to bar() may be an expensive one. Avoiding useless work is beneficial to performance. There is however a more fundamental reason. Assuming that bar() has side effects, the meaning of the tuple example is different from the nested if let expressions because in the case of the former, the side effect of bar() always happens while it will not if let A(x) = foo() does not match.

The difference between the tuple example and if let-chains become even greater if we also consider a dependence between foo() and bar(..) as in the following example:

if let A(x) = foo() {
    if let B(y) = bar(x) {
        computation_with(x, y)
    }
}

Calling bar(x) is now dependent on having an x that is only available to us by first pattern matching on foo(). Therefore, there is no tuple-based equivalent to the above example. With this RFC implemented, you can more ergonomically write the same expression as:

if let A(x) = foo()
    && let B(y) = bar(x)
{
    computation_with(x, y)
}

The new expression form introduced by this RFC is also not limited to simple if let expressions, you may of course also add else branches as seen in the example below.

if let A(x) = foo()
   && let B(y) = bar()
{
    computation_with(x, y)
} else {
    alternative_computation()
}

While the below snippet is not what the compiler would desugar the above one to, you can think of the former as semantically equivalent to it. The compiler is free to not actually emit two calls to alternative_computation() in your compiled binary. For details, please see the reference-level-explanation.

if let A(x) = foo() {
    if let B(y) = bar(x) {
        computation_with(x, y)
    } else {
        alternative_computation()
    }
} else {
    alternative_computation()
}

As briefly explained above, the if let-chain expression form is also not limited to pattern matching. You can also mix in any number of conditionals in any place you like, as done in the example below:

if independent_condition
   && let A(x) = foo()
   && let B(y) = bar()
   && y.has_really_cool_property()
{
    computation_with(x, y)
}

The above example example is semantically equivalent to:

if independent_condition {
   if let A(x) = foo() {
       if let B(y) = bar() {
           if y.has_really_cool_property() {
                computation_with(x, y)
            }
        }
    }
}

Naturally, inside an if-let-chain expression, a let binding must come before it is referred to. As such, the following snippet would be ill-formed since we haven't implemented time-travel (yet):

if y.has_really_cool_property() // <-- y used before bound.
   && let B(y) = bar(x) // <-- x used before bound.
   && let A(x) = foo()
{
    computation_with(x, y)
}

while let-chains

A while let-chain is similar to an if let-chain but instead applies to while let expressions.

Since we've already introduced the basic idea previously with if let-chains, we will jump straight into a more complex example.

The popular itertools crate has an izip macro that allows you to "Create an iterator running multiple iterators in lockstep". An example of this, taken from the documentation of izip is:

#[macro_use] extern crate itertools;

// iterate over three sequences side-by-side
let mut results = [0, 0, 0, 0];
let inputs = [3, 7, 9, 6];

for (r, index, input) in izip!(&mut results, 0..10, &inputs) {
    *r = index * 10 + input;
}

assert_eq!(results, [0 + 3, 10 + 7, 29, 36]);

With this RFC, we can write this, admittedly not as succinctly, as:

let mut results = [0, 0, 0, 0];
let inputs = [3, 7, 9, 6];

let r_iter = results.iter_mut();
let c_iter = 0..10;
let i_iter = inputs.iter();

while let Some(r) = r_iter.next()
    && let Some(index) = c_iter.next()
    && let Some(input) = i_iter.next()
{
    *r = index * 10 + input;
}

assert_eq!(results, [0 + 3, 10 + 7, 29, 36]);

The loop in the above snippet is equivalent to:

loop {
    if let Some(r) = r_iter.next()
        && let Some(index) = c_iter.next()
        && let Some(input) = i_iter.next()
    {
        *r = index * 10 + input;
        continue;
    }
    break;
}

Notice in particular here that just as we could rewrite while let in terms of loop + if let, so too can we rewrite while let-chains with loop + if let-chains.

While these two first snippets are equivalent in this example, this does not generally hold. If i_iter.next() has side effects, then those will not happen when Some(index) does not match. This is important to keep in mind. Short-circuiting still applies to while let-chains as with if let-chains.

Reference-level explanation

This RFC introduces if let-chains and while let-chains in Rust 2018 and makes some enabling preparation for such chains in Rust 2015.

Grammar

We replace the following productions:

block_expr
: expr_match
| expr_if
| expr_if_let
| expr_while
| expr_while_let
| expr_loop
| expr_for
| UNSAFE block
| path_expr "!" maybe_ident braces_delimited_token_trees
;

expr_if
: IF expr_nostruct block
| IF expr_nostruct block ELSE block_or_if
;

expr_if_let
: IF LET pat "=" expr_nostruct block
| IF LET pat "=" expr_nostruct block ELSE block_or_if
;

block_or_if : block | expr_if | expr_if_let ;

expr_while : maybe_label WHILE expr_nostruct block ;
expr_while_let : maybe_label WHILE LET pat "=" expr_nostruct block ;

with:

block_expr
: expr_match
| expr_if
| expr_while
| expr_loop
| expr_for
| UNSAFE block
| path_expr "!" maybe_ident braces_delimited_token_trees
;

expr_if
: IF in_if_list block
| IF in_if_list block ELSE block_or_if
;

block_or_if : block | expr_if ;

expr_while : maybe_label WHILE in_if_list block ;

in_if
: "let" pat "=" expr_nostruct
| expr_nostruct
| "(" in_if ")"
;

in_if_list : in_if [ ANDAND in_if ]*

Dealing with ambiguity

There exists an ambiguity in this new grammar in how to parse:

if let PAT = EXPR && EXPR { .. }

It can either be parsed as (1):

if let PAT = (EXPR && EXPR) { .. }

or instead as (2):

if (let PAT = EXPR) && EXPR { .. }

In the interest of succinctness, we do not encode a grammar here that resolves this ambiguity. Nonetheless, interpretation (2) is always chosen.

As specified in the reference in the section on expression operator precedence, the following operators all have a lower precedence than &&:

  • ||
  • .. and ..=
  • =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
  • return, break

To be precise, the changes in this RFC entail that || has the lowest precedence at the top level of if STUFF { .. }. The operator && has then the 2nd lowest precedence and binds more tightly than ||. If the user wants to disambiguate, they can write (EXPR && EXPR) or { EXPR && EXPR } explicitly. The same applies to while expressions.

A few more examples

Given:

if let Range { start: _, end: _ } = true..true && false { ... }

if let PAT = break true && false { ... }

if let PAT = F..|| false { ... }

if let PAT = t..&&false { ... }

it is currently interpreted as:

if let Range { start: _, end: _ } = true..(true && false) { ... }

if let PAT = break (true && false) { ... }

if let PAT = F..(|| false) { ... }

if let PAT = t..(&&false) { ... }

but will be interpreted as:

if (let Range { start: _, end: _ } = true..true) && false { ... }

if (let PAT = break true) && false { ... }

if (let PAT = F..) || false { ... }

if (let PAT = t..) && false { ... }

Rollout Plan and Transitioning to Rust 2018

Everything in this section also applies to while let expressions.

To enable the second interpretation in the previous section a warning must be emitted in Rust 2015 informing the user that:

if let PAT = EXPR && EXPR ...? { .. }

if let PAT = EXPR || EXPR ...? { .. }

will both become hard errors, in the first version of Rust where the 2018 edition is stable, without the let_chains features having been stabilized.

Note that this applies when there's at least one && or || operator at the top level of the RHS. This means that it does not apply in, among others, the following cases:

if let PAT = ( EXPR && EXPR ) { .. }

if let PAT = { EXPR && EXPR } { .. }

if let PAT = ( EXPR || EXPR ) { .. }

if let PAT = { EXPR || EXPR } { .. }

since the user has disambiguated the intent explicitly.

Pending the stabilization of the features in this RFC, to opt into the new semantics, users will need to use a nightly compiler and add the usual feature gate opt-in.

Facilitating for macro authors

To facilitate for macro authors, we permit the following:

if (let PAT = EXPR) && ... { ... }

let PAT = EXPR is not an expression

Note that let PAT = EXPR does not become an expression (typed at bool) with this RFC. Thus, you may not write:

let foo: bool = let Some(_) = None;
let bar: bool = let Some(_) = Some(1);
assert_eq!(foo, false);
assert_eq!(bar, true);

Semantics of if let-chains

The semantics of if let-chains can be understood by an in-surface-language desugaring using only RFC 2046 and if let.

The following:

if let PAT_1 = EXPR_1
    && let PAT_2 = EXPR_2
    && EXPR_3
    ...
    && let PAT_N = EXPR_N
{
    EXPR_IF
} else {
    EXPR_ELSE
}

desugars into:

'FRESH_LABEL: {
    if let PAT_1 = EXPR_1 {
        if let PAT_2 = EXPR_2 {
            if EXPR_3 {
                ...
                if let PAT_N = EXPR_N {
                    break 'FRESH_LABEL { EXPR_IF }
                }
            }
        }
    }
    { EXPR_ELSE }
}

This avoids any code duplication and requires no new semantics. The rules for borrowing and scoping are just those that result directly from the desugar.

The else if branches:

if let PAT_1 = EXPR_1
    && let PAT_2 = EXPR_2
{
    EXPR_IF
} else if let PAT_3 = EXPR_3
    && EXPR_4
{
    EXPR_ELSE_IF
} else {
    EXPR_ELSE
}

are defined by their desugaring to:

'FRESH_LABEL: {
    if let PAT_1 = EXPR_1 {
        if let PAT_2 = EXPR_2 {
            break 'FRESH_LABEL { EXPR_IF }
        }
    }

    if let PAT_3 = EXPR_3 {
        if EXPR_4 {
            break 'FRESH_LABEL { EXPR_ELSE_IF }
        }
    }

    { EXPR_ELSE }
}

Having an else branch is optional. The following example without an else branch:

if let PAT_1 = EXPR_1
    && let PAT_2 = EXPR_2
{
    EXPR_IF
}

is simply desugared into:

if let PAT_1 = EXPR_1 {
    if let PAT_2 = EXPR_2 {
        EXPR_IF
    }
}

If we have an else if branch but no else branch, such as in this example:

if let PAT_1 = EXPR_1
    && let PAT_2 = EXPR_2
{
    EXPR_IF
} else if let PAT_3 = EXPR_3
    && EXPR_4
{
    EXPR_ELSE_IF
}

the semantics are defined by the following desugaring:

'FRESH_LABEL: {
    if let PAT_1 = EXPR_1 {
        if let PAT_2 = EXPR_2 {
            break 'FRESH_LABEL { EXPR_IF }
        }
    }

    if let PAT_3 = EXPR_3 {
        if EXPR_4 {
            break 'FRESH_LABEL { EXPR_ELSE_IF }
        }
    }
}

Semantics of while let-chains

The semantics of while let-chains can be understood by an in-surface-language desugaring using only RFC 2046, loop and if let.

For example:

while EXPR_1
    && let PAT_2 = EXPR_2
    && let PAT_3 = EXPR_3
    && EXPR_4
{
    EXPR_WHILE
}

is defined by desugaring into:

loop {
    if EXPR_1
        && let PAT_2 = EXPR_2
        && let PAT_3 = EXPR_3
        && EXPR_4
    {
        { EXPR_WHILE }
        continue;
    }
    break;
}

This desugaring relies on the previously discussed desugaring for if let-chains.

More generally, we may desugar:

while in_if_list {
    EXPR_WHILE
}

into:

loop {
    if in_if_list {
        { EXPR_WHILE }
        continue;
    }
    break;
}

Drawbacks

This RFC mandates additions to the grammar as well as adding syntax lowering passes. These are small additions, but nonetheless the language specification is possibly made more complex by it. While this complexity will be used by some and therefore, the RFC argues, motivates the added complexity, it will not be used all users of the language. However, as discussed in the motivation, by unifying constructs in the language conceptually and grammatically, we may also say that complexity is reduced.

When it comes to if let-chains, the feature is already supported by the macro if_chain!. Some may feel that this is enough.

It should also be taken into account that some breakage will occur as a result of this RFC. Sergio Benitez has however done some review of the crates.io ecosystem and found zero cases of actual breakage. At any rate, writing let PAT = EXPR && .. as a user is a bad thing to do.

Finally, some may argue, as done by @petrochenkov, that this is "a lot of ad-hoc syntax to deprecate when the proper solution solving all the listed problems is implemented".

Rationale and alternatives

We will now discuss how and why this RFC came about in its current form.

The impact of not doing this

There are at least two sides to power in language expressivity:

  1. The ability to express something in a language at all.

  2. The ability to express something with ease.

Nothing proposed in this RFC adds to point 1. While this is the case, it is not sufficient. The second point is important to make the language pleasurable to use and this is what this RFC is about. Not including the changes proposed here would keep some paper cuts around.

Design considerations

There are some design considerations on this feature. These are:

  1. the syntax mixes well with normal boolean conditionals.

  2. the additions be simple conceptually and build on what language
    users already know.

  3. as little of the complexity budget as possible is used.

  4. the bindings bound in the pattern have a clear and consistent scope.

  5. the short-circuiting nature is clear.

  6. instead of a heap of special cases, the grammar should be simple.

With these considerations in mind, the RFC was developed.

Note that these are considerations and have different levels of importance. Note also that it is likely impossible to meet all of them, but we'd like to tick as many boxers as possible.

Keeping the door open for if-let-or-expressions

Should a user be able to write something like the following snippet?

if let A(x) = e1
    || let B(x) = e2 {
    do_stuff_with(x)
} else {
    do_other_stuff()
}

What does this expression even mean? It means that if one of the patterns match, then the first one of those will bind a value to x and the expression evaluates to do_stuff_with(x). If no patterns match, the expression instead evaluates to do_other_stuff().

This RFC does not propose such a facility, but does not foreclose such a possibility, making the feature future proof and allowing discussion on such a facility in the future to continue. Alternatives should similarly try to retain this ability.

Alternative: RFC 2046, label break value

RFC 2046, which has been merged but not stabilized, is a more general control flow graph (CFG) control feature. While it doesn't as straightforwardly solve the rightward drift or ergonomic issues as this RFC does, it allows the macros to be improved by removing duplication of else blocks. The closest syntax today for that is loop-break, but that doesn't work as continue is intentionally non-hygienic.

RFC 2046 is also a bit orthogonal in the sense that it's fully compatible with this RFC. The general label break is useful and powerful, as seen in the reference-level-explanation of this RFC and of catch's, but is verbose and unfamiliar. Having a substantially more ergonomic feature for this particularly common case is valuable regardless. As such, we argue that this RFC is mostly complementary wrt. RFC 2046.

Furthermore, as we've noted in the motivation, a macro based approach is not a construct that is universal among Rust programmers, which is an important property for control flow in particular to improve the legibility of programs.

The main alternatives

There are some alternatives to consider. Let's go through some of the main ones.

First, there's the choice of a separator to use in-between lets and bool typed condition expressions. We consider 3 different separators:

  1. logical and (&&)
  2. comma (,)
  3. if

We also consider two different ways to bind inside if:

  1. let PAT = EXPR
  2. EXPR is PAT

Additionally, instead of the keyword is, we consider match. In total, we have 6 (or 9 if we count match) variants to pick from. These 6 alternatives are:

In this RFC, we propose the combination of && and let PAT = EXPR.

A survey - Method

To gain some data on what users of Rust think about the 6 different variants, a multi-answer survey was done using Google Forms. The survey ran from 2017-12-31 06:25 to ~2018-01-06 ~14:00 and received 373 answers. Participants were also able to provide free-form motivation ("comments") to their answers if they so wished.

To decrease the risk of bias in favour of a particular alternative, the order of the answers as presented to survey participants were randomized. Furthermore, to make the survey more fair, all alternatives were syntax highlighted as a normal IDE would do.

The survey answers had the following distribution in origin:

  • Reddit, 68.4%
  • internals.rust-lang.org, 16.6%
  • users.rust-lang.org, 7.5%
  • IRC, 5.1%
  • The RFC, 2.4%

A survey - Data

For those interested in reading the survey answers you can do so by reading:

The breakdown of preferences were:

  1. Using && and let PAT = EXPR - liked: 66.2%, disliked: 16.9%

    if let PAT = EXPR
        && let PAT = EXPR
        && EXPR
    {
        ..
    }
    
  2. Using && and EXPR is PAT - liked: 24.9%, disliked: 48.5%

    if EXPR is PAT
        && EXPR is PAT
        && EXPR {
        ..
    }
    
  3. Using , and let PAT = EXPR - liked: 16.9%, disliked: 56.3%

    if let PAT = EXPR,
       let PAT = EXPR,
       EXPR {
        ..
    }
    
  4. Using if and let PAT = EXPR - liked: 12.3%, disliked: 66%

    if let PAT = EXPR
    if let PAT = EXPR
    if EXPR {
        ..
    }
    
  5. Using , and EXPR is PAT - liked: 4.3%, disliked: 74.5%

    if EXPR is PAT,
       EXPR is PAT,
       EXPR {
         ..
    }
    
  6. Using if and EXPR is PAT - liked: 2.4%, disliked: 80.4%

    if EXPR is PAT
    if EXPR is PAT
    if EXPR {
        ..
    }
    

Finally, 9.7% liked none of the options and 1.9% liked all of them.

A survey - Analysis of Comments

There are too many answers to include here, instead, we select some of the most interesting ones and highlight them.

Tried before

One participant, among 6 (see Appendix B.1) others who all positively inclined, explicitly commented that they had tried the syntax proposed in this RFC before.

The "if let .. && let .. && .." feels like the intuitive way to do it if you don't think about the language syntax too much. It's definitely the way I tried doing it when I thought it was possible at the start of my Rust path.

This substantiates the claim made in the motivation.

Consistency

An even greater number of people (48, see Appendix B.2) commented that they thought that the proposed syntax was the consistent alternative. This was by far the most frequent comment made in the survey.

So I like that using && is how we currently use it in the language, and everyone is already used to using let A(x) = foo(). Honestly, the one I chose feels the most consistent with the language.

Intuitiveness

A lesser number (8, see Appendix B.3) of participants said did not explicitly say that the proposed syntax was consistent, but that they found it intuitive nonetheless.

&& makes the logic relationship clearer, and using let for binding is the same. Conjunction is more readable with &&

This, and in particular the consistency, goes a long way to satisfy points 2-3 in the design considerations.

Expectation that (let PAT = EXPR) : bool

A few participants (3, see Appendix B.4) hinted at that using && together with let PAT = EXPR set up the expectation that the latter is a bool typed expression.

Using && for conjunction with let PATTERN = EXPR feels consistent with the existing if let syntax, however it causes potentially some confusion about data types and its existing function as a boolean operator, so that leads me to considering , as the conjunction instead. However, if "let PATTERN = EXPR" is an expression returning a boolean as well as setting up the pattern bindings then there's no issue with && at all, and it's then preferable to me provided it's available where you'd expect expressions to be available and not treated particularly specially.

If that were the case you'd be able to write:

let is_some: bool = let Some(_) = the_option;

However, this is not the case in this proposal.

We expect that this will be one of the most frequent misconceptions in relation to the proposed syntax. However, such misconceptions can be put to bed simply when the user tries to write a snippet like the one above. They will then get an error message that clears up that misconception. It should also be noted that if let, which exists in the language today, also suffers from this problem. That is, given if let PAT = EXPR { .. }, a user may get the impression that it is the composition of if EXPR { .. } and let PAT = EXPR while it is not. While the syntax changes in this RFC does enhance the risk of misconception somewhat, ultimately we do not feel that it poses a critical problem.

Commas and if as separators - conjunction?

There were many people (19, see Appendix B.5) who felt that using , or if as the separator did not clearly enough signal conjunction and thought that the symbols may be mistaken for disjunction.

Commas just aren't clear enough: on their own, to many people, they could easily be interpreted as logical ORs or logical ANDs.

In most cases, these comments were directed towards ,, but there were also some who thought this about if:

if after if with no logical operator? is this AND? is this OR?

On the other hand, it could be argued that Rust already uses if for conjunction since you can use PAT if EXPR => .. inside match expressions. Indeed, a few people hinted at this:

  1. Clear and unambiguous, and similar to existing guards in match statements, so it does not introduce completely new syntax.

  2. This is already basically how match arms work.

Our conclusion is that this at least presents a serious enough of a problem for , as the separator for conjunction to rule it out while also being problematic for if.

Commas and short-circuiting

A number of participants (5, see Appendix B.6) noted that using , as the separator was not clearly enough indicating short-circuiting behaviour.

On the other hand the comma'd version felt the least clear in meaning and execution order. I'm more used to things-separated-by-commas being roughly equivalent instead of being something that ends up short circuiting the evaluation.

This is a further blow to , in terms of our design considerations.

if as separator is noisy

Some people argued that if as a separator felt noisy or that it felt like there were missing braces. One also noted that multiple ifs on one line wouldn't work well on a single line. However, one respondent said that the "eliding of braces"-interpretation was a good thing.

As an aside, we would like to note here that if as a separator would need to be matched with while as a separator as well. This makes the separator too context dependent in our view.

Patterns unexpectedly on the RHS

Some people (10, see Appendix B.8) thought that bindings introduced on the RHS as in EXPR is PAT as opposed to let PAT = EXPR was backwards and weird.

expr is pat reverses the directionality for pattern bindings seen everywhere else in Rust;

One could argue that bindings introduced in the arms of match expressions are to the right if one formats such expressions as:

match EXPR { PAT => ... }
   // LHS // RHS

However, this is not the typical formatting of match expressions as they tend to include more than one arm. When using the normal formatting of such expressions, the match arms, and therefore the bindings, are introduced on the LHS.

This inconsistency does not have to be an insurmountable problem as we believe that EXPR is PAT generally reads well. However, having the pattern consistently on LHS everywhere makes introductions of bindings more readily scannable, which is a valuable property when reading code quickly.

The is operator introduces bindings

However, a more serious problem that some survey participants (15, see Appendix B.9) identified was that EXPR is PAT, according to the respondents, confusingly introduces a binding and that it could be misconstrued as an equality test of some sort.

is doesn't make any sense since we already have if let PATTERN and is in other languages is typically a reference equality check (e.g. Dart and Python).

I dislike the EXPR is PATTERN syntax because while the word let indicates that there is some binding going on, I read the word is as passively checking whether the expression fits a pattern without binding. I also dislike is because it is new syntax that does the same thing as existing syntax.

We believe this problem to be more serious. As an alternative to EXPR is PAT, some have proposed using the existing keyword match instead. You would then instead write the example in the motivation as:

fn param_env<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>, def_id: DefId) -> ParamEnv<'tcx> {
    if tcx.describe_def(def_id) match Some(Def::Existential(_))
        && tcx.hir.as_local_node_id(def_id) match Some(node_id)
        && tcx.hir.get(node_id) match hir::map::NodeItem(item)
        && item.node match hir::ItemExistential(ref exist_ty)
        && exist_ty.impl_trait_fn match Some(parent)
    {
        return param_env(tcx, parent);
    }

    ...
}

As previously noted, using is is less scannable. This also applies to match.

As an aside, one survey participant confused is for as; This does seem like a mistake that is likely to happen due to the similarity of these two words.

Conclusion

We believe that the case for && and let PAT = EXPR is strong. As demonstrated by the survey, which we believe is statistically significant, it is both consistent and intuitive for most users. The syntax also satisfies most of the points in the design considerations.

The only main drawbacks to this proposal is some tiny bit of breakage as well as an increase in implementation complexity. The breakage is considered OK, because writing let true = p && q is at any rate a terrible style and because it is so infrequent. As for the increased grammar complexity, we believe this is less important in this case than making control flow more ergonomic and readable for users.

Some may view the fact that let PAT = EXPR is not an expression typed at bool as an ad-hoc solution. However, we believe that we should live within our means wrt. the complexity budget and spend it on more important things. Furthermore, as evidenced in RFC 2260, making EXPR is PAT, which has other problems we've previously noted, an expression is also tricky due to the non-obvious scoping rules for bindings it entails. Mainly because of this, support for EXPR is PAT has been slow to develop.

For the use case of having some pattern matching construct that is typed at bool, we could later introduce the form EXPR is PAT but prohibit PAT from introducing bindings.

Prior art

Swift

The expression form if let PAT = EXPR { .. } was introduced to Rust by accepting RFC 160. That RFC noted that:

The if let construct is based on the precedent set by Swift, which introduced its own if let statement. In Swift, if let var = expr { ... } is directly tied to the notion of optional values, and unwraps the optional value that expr evaluates to. In this proposal, the equivalent is if let Some(var) = expr { ... }.

As the construct if let was inspired by Swift, it therefore makes sense to consult Swift to see how the language deals with multiple lets in if.

It turns out that you can by writing:

if let g = greetings, let s = salutations {
    print(g)
    print(s)
}

which with the syntax proposed in this RFC would be equivalent to:

if let Some(g) = greetings
    && let Some(s) = salutations
{
    print(g)
    print(s)
}

You can also use case let for more general pattern matching:

if case let Media.movie(_, _, year) = m, year < 1888 {
    ...
}

Previously in Swift, you would instead write:

if case let Media.movie(_, _, year) = m, where year < 1888 {
    ...
}

but this was changed in favour of omitting where in SE-0099.

Interestingly, the separator token that Swift uses for conjunctive chaining in if is , (comma). RFC 2260 proposed this, but this turned out not to be as intuitive for many users as && is (see alternatives for a discussion).

Kotlin

In RFC 2260 @matklad said that:

It's interesting to compare it with Kotlin, which also uses is operator for the similar purpose: https://kotlinlang.org/docs/reference/typecasts.html#smart-casts.

The differences is that instead of destructing, Kotlin's is supplies a flow-sensitive type information. The compiler indeed uses pretty smart control-flow analysis to check if every use of a variable is dominated by the is check.

However, as long as the compiler does all the inference work for you, actually using this feature is easy: you don't have to replay the analysis in your head when reading or writing code, because the compiler catches all errors.

RFC 160

Interestingly, the EXPR is PAT idea was floated in the original RFC 160 that introduced if let expressions in the first place. There, the notion that an operator named is, which introduces bindings, is confusing was brought up.

It was also mentioned by @lilyball that it would be appropriate if, and only if, it was limited to pattern matching, but not introducing any bindings. We make the same argument in this RFC. The issue of unintuitive scopes was also mentioned by @lilyball there.

Even the idea of if EXPR match PAT was floated by @liigo at the time but that idea was ultimately also rejected. @lilyball opined that using match as a binary operator would be "very confusing" but did not elaborate further at the time.

Unresolved questions

The final syntax

The main goal of this RFC is threefold:

  1. Decide that this is a problem that needs to be solved somehow.

  2. Make the proposed syntax in the RFC an option that is available in Rust 2018.

  3. Adopt the proposed syntax in the RFC.

Of these points, the 1st and the 2nd are the most important for the time being. The 3rd point is not unimportant, but it is not as time sensitive. Thus, one path ahead of least resistance is to adopt the syntax in the RFC and make it available in Rust 2018 while leaving the final syntax unresolved. We can then debate alternatives, in particular using EXPR match PAT, more rigorously post shipping Rust 2018. Finalizing the syntax and can then be decided in a tracking issue or another RFC.

Irrefutable let bindings after the first refutable binding

Should temporary and irrefutable lets without patterns be allowed as in the following example?

if let &List(_, ref list) = meta
    && let mut iter = list.iter().filter_map(extract_word) // <-- Irrefutable
    && let Some(ident) = iter.next()
    && let None = iter.next()
{
    *set = Some(syn::Ty::Path(None, ident.clone().into()));
} else {
    error::param_malformed();
}

With normal if let expressions, this is an error as seen with the following example:

fn main() {
    if let x = 1 { 2 } else { 3 };
}

Compiling the above ill-formed program results in:

error[E0162]: irrefutable if-let pattern

However, with the implementation of RFC 2086, this error will instead become a warning. This is understandable - while the program could have perfectly well defined semantics, where the value of the expression is always 2, allowing the form would invite some developers to write in a non-obvious way. A warning is however a good middle ground.

However, when let bindings in the middle are irrefutable, there is some value in not warning against the construct. In the case of the initial example in this subsection, it would be written as follows without irrefutable let bindings:

if let &List(_, ref list) = meta {
   let mut iter = list.iter().filter_map(extract_word);
    if let Some(ident) = iter.next()
        && let None = iter.next()
    {
        *set = Some(syn::Ty::Path(None, ident.clone().into()));
    } else {
        error::param_malformed();
    }
} else {
    error::param_malformed();
}

However, now we have introduced rightward drift and duplication again, which we wanted to avoid.

On the other hand, allowing irrefutable patterns in the middle without a warning may give the impression that the irrefutable pattern is refutable, or cast doubt on it making semantics possibly harder to grasp quickly.

This is a tricky question, which we leave open for consideration during the stabilization period or even after stabilization.

Chained if lets inside match arms

Would the following be accepted by a Rust compiler?

match EXPR {
    PAT if let PAT = EXPR && EXPR && ... => { .. }
    _ => { .. }
}

The combination of the accepted, but yet to be stabilized, RFC 2294, and this RFC would entail that it would be accepted. However, at this point, and in the interest of time, we leave this for a future RFC or for pre-stabilization.

Appendix A - Style considerations

How should the features introduced in this RFC be formatted? This is not a make or break question but rather a style question for rustfmt. What you read here should not be taken as prescriptive but rather as discussion material and to generate ideas. Any eventual decision on style will be made by a separate style RFC.

Here are a few variants on indentation to consider for rustfmt while may or may not be mutually compatible:

1. && on a new line and indented + Open-brace after newline

if independent_condition
    && let Alan(x) = turing()
    && let Alonzo(y) = church(x)
    && y.has_really_cool_property()
{
    computation_with(x, y)
}

This style is maximally consistent with how conditions in if expressions are currently formatted.

Moving the open brace down a line may help emphasize the split between a lengthy condition and the block body.

2. && after bindings

if independent_condition &&
   let Haskell(x) = curry() &&
   let Alonzo(y) = church(x) &&
   y.has_really_cool_property() {
    computation_with(x, y)
}

This style is consistent with how separators, such as ,, are currently formatted in Rust.

3. && at the start of lines

if independent_condition
&& let Alan(x) = turing()
&& let Alonzo(y) = church(x)
&& y.has_really_cool_property() {
    computation_with(x, y)
}

This style of leading separators is inconsistent with current formatting.

4. Aligning the equals sign together

if independent_condition &&
   let Alan(x)   = turing() &&
   let Alonzo(y) = church(x) &&
   y.has_really_cool_property() {
    computation_with(x, y)
}

While this might look visually pleasing, visual indent like this is against the rustfmt guidelines.

5. Newline after else if

if independent_condition &&
   let Conor(x) = mcbride() &&
   let Euginia(y) = cheng(x) &&
   y.has_really_cool_property() {
    computation_with(x, y)
} else if // <-- Notice newline.
    let Stephanie(x) = weirich() &&
    let Thierry(y) = coquand() {
    computation_with(x, y)
}

In this version we look at whether or not a newline should be inserted after an else if branch. The benefit of inserting a newline is that it aligns well with the let bindings in the if branch.

6. No indent at all, just a list of conditions

if independent_condition &&
let Alan(x)   = turing() &&
let Alonzo(y) = church(x) &&
y.has_really_cool_property() {
    computation_with(x, y)
}

In this version, we do not indent the lets and the boolean side-conditions. But we do place the && on the end of lines. One benefit here is that the body of the if expression more clearly stands out. However, a drawback is that the if token stands less out.

There are of course more versions one can contemplate and the various combination of them, but in the interest of brevity, we keep to this list here.

Appendix B

This appendix groups some survey answers together for the purposes of analysis. Please note that this appendix is by no means complete and is only offered on a best-effort basis. The comments cited below have also been cleaned up to fix obvious spelling mistakes, etc.

Appendix B.1

Here are a number of participants in the survey commenting that they expected the proposed syntax in this RFC to work.

  1. The "if let .. && let .. && .." feels like the intuitive way to do it if you don't think about the language syntax too much. It's definitely the way I tried doing it when I thought it was possible at the start of my Rust path.

  2. I tried to write this one and then realized it's not supported.

  3. I've already tried to do this before and find it didn't work.

  4. I was surprised to find that this syntax wasn't already supported. Principle of least surprise for the win.

  5. I would expect "Using && for conjunction and let PATTERN = EXPR" to work already today

  6. I tried to used this specific syntax and I expected it to work already.

  7. let … matches the current if let, and the && matches the way I would write it. I've tried to write if let Some(x) = foo && x.bar() { … } before.

Appendix B.2

Many participants in the survey opined that the proposed syntax was consistent with current Rust. They thought that this was positive.

  1. It is the only option consistent with what we have today and expect once we learn about let PATTERN = EXPR.

  2. Close to current Rust syntax

  3. Most similar to existing syntaxes, which increases orthogonality.

  4. This seems most consistent with existing Rust syntax.

  5. consistency with current Rust

  6. consistency with current syntax

  7. Consistency with current syntax

  8. It's the least surprising syntax. It's obvious.

  9. Should be consistent and similar to how match patterns/existing let A(x) = b works.

  10. Seems to most closely match existing syntax and style

  11. Seems consistent with existing syntax

  12. Consistent with current Rust syntax.

  13. Consistency with the syntax we already have.

  14. compatibility with current syntax

  15. It's feels consistent with the rest of the language.

  16. Consistency with existing Rust constructs and familiarity with C and Swift syntax.

  17. consistent with already existing if and let patterns. intuitive

  18. close to current rust syntax (same assign syntax as in match and if let)

  19. I chose "Using && for conjunction and let PATTERN = EXPR" because it seems like the only choice that is consistent with Rust syntax as it is today. The rest are... strange.

  20. we already have while let A(x) = foo() and && in if statements, I don't see how any other syntax makes sense

  21. Using && unambiguously means conjunction and is, IMO, easier to read. let PATTERN = EXPR does not introduce a new form of pattern matching to the language.

  22. The "let(x) = expr" is consistent with the current syntax, the "&&" makes it clear it's an AND (and in most languages it's short-circuited).

  23. So I like that using && is how we currently use it in the language, and everyone is already used to using let A(x) = foo(). Honestly, the one I chose feels the most consistent with the language.

  24. double & is standard for logical AND, "if let foo(x) = bar" is just as good as the other syntax but is already standard in rust, so might as well keep it

  25. using && and let PATTERN = EXPR is more intuitive because you're checking a condition and whether a pattern matches.

  26. follows standard "&&" pattern and "if-let" pattern as well

  27. uses existing syntax

  28. It's what I already know in Rust

  29. this is the most similar to rust's current if let syntax

  30. Uses already established keywords and operators in a semantically similar way.

  31. It just looks like normal rust we're all used to (if let destructuring syntactic sugar)

  32. The most natural extension of let expressions and boolean conditions

  33. The && operator and "if let" are already in the language. No reason to pick something totally different. More on that on the next page.

  34. I don't really like any of them. I prefer ; for conjunction because that's more similar to how Go does it, though && for conjunction and let PATTERN = ... is okay because it's intuitive given other language features in Rust.

  35. Using && for conjunction is consistent with other languages I know, using let for pattern matching is explicit about introducing new names.

  36. Seems the most natural. If I knew if let x = y could be combined with other conditions, my first thought would be && z == z1

  37. Smallest delta from current syntax. && already exists, let PATTERN = EXPR exists, just allowing the two in composition.

  38. The option I chose (if expr && let pat = expr && let pat = expr && expr { body }) is the most consistent with existing Rust syntax. It's a fairly natural extension of the if let syntax since it uses let pat = expr in a place where you could otherwise use expr. Using && as a conjunction most clearly expresses the intention IMO, and it also clearly follows short-circuit evaluation.

  39. EXPR is PATTERN is introducing new alternative syntax which is useless when we already have the let PATTERN = EXPR syntax. && is also clearly the best choice for joining conditions because that is what it is already used for!

  40. "if let" is already a well-known thing in Rust, so keep it. Conjunction is already a well-known thing in Rust, so keep it. In short, make minimal changes to the language that make the example work.

  41. Using && for conjunction along with the existing syntax for let bindings is the most intuitive and feels the least like it's special-casing. I think this is less likely to confuse beginners, and makes it feel more cohesive.

  42. Follows the standards of current syntax relatively closely without introducing new symbols, and builds on the existing understanding of let-deconstruction while clearing showing (through the use of let) that we have assigned x and y.

  43. This is the syntax that I would expect without reading the manual.

  44. I feel that including let is important to make it clear that the pattern is exposing the variables x and y for use in the block body and && is by far the most intuitive way to AND together test conditions. In fact, presenting alternatives to Rust's existing && syntax for ANDing together terms made even the use of && confusing because the claim that they were all equivalent meant that it "couldn't possibly be" the existing meaning of &&. I didn't know what was going on until I realized I'd glossed over tiny (ie. unimportant) text which actually explained the meaning in plain English... at which point, I realized that the syntaxes other than && had set up a mistaken assumption that ruled out the actual proper interpretation.

  45. I like Using && for conjunction and let PATTERN = EXPR because it, for me, has the least surprises syntactically. && indicates conjunction of the predicate, and including x and y in subsequent scopes is something I wish existed, but if we're not clear about it, it could get messy.

  46. it does not introduce anything fancy new stuff

  47. I like the "let" syntax better than the EXPR is PATTERN syntax, since it's used in other places already.

    1. && is already a familiar concept for working with boolean expressions
    2. if let is how we already achieve conditional binding

    The combination of "is" and && is the only other choice I could consider, albeit begrudgingly. I'm kind of uncomfortable with giving up keyword real estate and having another way of doing if let.

    Other than that, I feel like the other choices alienate both new and old Rust programmers alike. We should be focusing on keeping things as simple and familiar as possible.

Appendix B.3

Some survey participants did not explicitly say that the RFC's proposed syntax was consistent, but they did say, in some way, that it was intuitive.

  1. && is the logical conjunction operator and let A(x) = foo clearly destructures for pattern matching

  2. The most intuitive

  3. It is not surprising

  4. Looks like straightforward boolean logic, the rest seem like arcane syntax.

  5. Reminiscent of boolean algebra

  6. It fits with my mental model of how patterns and Boolean logic work

  7. && makes the logic relationship clearer, and using let for binding is the same. Conjunction is more readable with &&

  8. We already have “if let” elsewhere. Dont introduce a new “is” syntax here, its not any more intuitive.

Appendix B.4

Some survey participants felt that the proposed syntax set up the expectation of let PAT = EXPR being an expression typed at bool as opposed to a statement which is currently the case.

  1. && as separator would require boolean expressions.

  2. && is for boolean expressions, and won't work right in generic usage. let would have to return a boolean which is weird and probably a breaking change.

  3. Using && for conjunction with let PATTERN = EXPR feels consistent with the existing if let syntax, however it causes potentially some confusion about data types and its existing function as a boolean operator, so that leads me to considering , as the conjunction instead. However, if "let PATTERN = EXPR" is an expression returning a boolean as well as setting up the pattern bindings then there's no issue with && at all, and it's then preferable to me provided it's available where you'd expect expressions to be available and not treated particularly specially.

Appendix B.5

Another group of people opined that , and if did not clearly imply conjunction and that it could be construed as disjunction instead. The majority of these comments were directed towards , as opposed to if.

  1. Commas do not feel like natural and separators.

  2. , is bad because it already means "separate things", and now it suddenly means "join things".

  3. , does not mean and to me

  4. Comma is not &&.

  5. "," doesn't seem like a conjunction (usually means tuple)

  6. , as conjunction is ambiguous (could just as well be disjunction)

  7. using a comma to mean conjunction is very unclear.

  8. I find the comma ambiguous (is it AND or OR?).

  9. Commas just aren't clear enough: on their own, to many people, they could easily be interpreted as logical ORs or logical ANDs.

  10. Although really the only reasonable interpretation of , is conjunction, it's still not immediately obvious that that is the case.

  11. The tower of ifs is quite ugly (although it seems less ambiguous than using commas, which to some people might be construed as disjunction).

  12. Ambiguous, are they 'or' or 'and'?

    note: this refers to , and not if as a separator.

  13. Commas don't imply conjunction to me and chained ifs just feel a bit unnatural too

  14. Using , is a bad idea because, with Rust already having a perfectly good &&, adding , is likely to evoke "OK, I know &&, so . must be OR" or "I know && and ||, so what the heck is ,? I'm so confused." ...not to mention that it runs against the Rust design philosophy to needlessly introduce alternative syntax and I can't see any practical reason it would be necessary to distinguish between tests and pattern matches in this context which can't be handled by putting let before the matches.

  15. these syntaxes don't make it clear that there is an and relationship between the conditions

  16. if after if with no logical operator? is this AND? is this OR?

  17. They either imply 'or' or remind me of a switch fall through in other languages (and thus also 'or')

  18. Stacking repeated uses of "if" at the top level feels very confusing to visually scan; it doesn't distinguish a conjunction very well.

  19. if for conjunction is confusing

Appendix B.6

Does , entail short-circuiting behaviour or not? Some survey participants did not think this was clear.

  1. I would also expect the comma options to not follow short-circuit evaluation.

  2. For users coming from other languages, comma is unclear about whether short-circuiting will take place.

  3. Syntax does not fit in with other usages of ',' in rust (especially tuples). It's non-obvious what the order of execution of sub-expressions are.

  4. The commas are out of left field: they bear no relation to anything currently in Rust or any other language. The conditional looks like some sort of tupling expression.

  5. On the other hand the comma'd version felt the least clear in meaning and execution order. I'm more used to things-separated-by-commas being roughly equivalent instead of being something that ends up short circuiting the evaluation.

Appendix B.7

A number of survey participants noted that separating with if is noisy and looks as if braces are missing.

  1. Using multiple ifs feels very weird (it looks like there are some missing braces and the indentation is wrong).

  2. Chaining if statements is unclear since in most languages you can leaves off the curly braces for an if with a single statement body.

  3. chaining "if" keywords without braces or separators doesn't convey the meaning of the statement well and seems out of place in rusts present syntax, even more so if contracted to a single line.

  4. Using a bunch of if in a column within the same if statement should stoke uncertainty about the intended meaning in anyone who remembers that Rust is very forgiving about where you put your whitespace.

  5. Too many ifs making it noisy.

Appendix B.8

A number of survey participants noted that bindings introduced in EXPR is PAT were unexpectedly on the RHS while they were used to it being on the LHS.

  1. Don't like 'is' since it puts variable binding on the right.

  2. is seems backwards.

  3. The 'is' operator creates new variables, but the pattern is on the right, where variables are usually read from.

  4. foo() is A(x) is backwards to binding in most other places.

  5. expr is pat reverses the directionality for pattern bindings seen everywhere else in Rust;

  6. Very unreadable, swapped order of unpacking confusing

  7. The "is" formulation is backwards from current if let.

  8. I really do no like how the is syntax has the left and right sides reversed from the if let ... = ... syntax. It seems very odd to have that sort of pattern matching written in opposite directions depending on the syntax you choose.

  9. The "is"-destructuring/pattern matching looks really weird because normally names have to be located on the left side of a statement to be bound to a value. The right side is there to retrieve the value.

  10. extracting with a pattern match is confusing when the pattern match is to the right of the variable being matched. it looks like a statement of fact, not the introduction of a new identifier.

Appendix B.9

Some survey participants opined that they found it surprising that an operator named is introduces bindings. Another group found that is could easily be confused for some sort of equality test (as in the operator ==) as in Python.

  1. using is to introduce new bindings is very surprising.

  2. is is weird because it can bind variables.

  3. The "is" syntax is confusing, since it does an implicit pattern binding. I think folks would get it wrong by trying to pass a bound variable there and being surprised to find that it's a pattern instead.

  4. I dislike the EXPR is PATTERN syntax because while the word let indicates that there is some binding going on, I read the word is as passively checking whether the expression fits a pattern without binding. I also dislike is because it is new syntax that does the same thing as existing syntax.

  5. Without let it isn't clear that we are declaring a new variable via is. Now we could introduce new keywords, but is still isn't clear about what it's doing. It seems odd to introduce is when if let does the same thing.

  6. The 'is' keyword suggests a boolean operation but silently behaves like a 'let'.

  7. If expr is pattern doesn't actually bind, and just pattern matches, then I like it. This should have been a language feature imo.

  8. I don't like is because it doesn't look like a binding operator

  9. The "is A(x)" syntax looked nice on first sight, but it's backwards, as in this case it's an assignment (to x and y) and not just a comparison. Maybe it's ok as "if foo() is A" (like for "if foo().is_some()" but more generic) but not in this case.

  10. I don't like using "is" for assignment. It sounds like equality (==), but with an assignment as a side effect. "if let" is the established way of doing equality and assignment together, and I think we should stick with one way of doing it. I also think "if let" better highlights that both equality and assignment happens, even when it is nested inside the expression as here.

  11. is doesn't make any sense since we already have if let PATTERN and is in other languages is typically a reference equality check (e.g. Dart and Python).

  12. is operator can be confusing (is the same as == or something else entirely?);

  13. is I don't like because it looks too much like subclass testing and/or identity testing from other languages. let I like for uniformity with if let and while let, but it needs something to make clear that the && isn't part of the thing being bound; maybe parens around the whole thing? Require parens around the whole RHS if there's an && anywhere in there? I don't know how to resolve the ambiguity... use match, instead?

  14. The "is" keyword is not in Rust yet (afaik) but if we wanted to use it, we should ponder that it means "reference equality" to Python people. I would thus be hesitant about using it for pattern matching expressions, especially given that we already have "let" for pattern matching. If possible, I would prefer making let-bindings an expression.

  15. Rust already has meanings for && and let which can be applied here. Replacing let ... with ... is ... is too different from existing pattern syntax and too similar to Python's identity testing operator.